Skip to content

Commit 994fb70

Browse files
nhortonclaude
andauthored
feat(review): inline reference files into review instructions + wire DeepSchema refs through (#353)
* feat(review): inline reference files into review instructions + wire DeepSchema refs through DeepSchema-generated reviews previously only referenced their examples, references, and json_schema_path by path, forcing reviewer agents to make a Read tool call for each external file at review time. Make reference-file inlining a generic feature of the review pipeline: - Add `reference_files: list[ReferenceFile]` to both ReviewRule and ReviewTask (src/deepwork/review/config.py). - `.deepreview` rules gain an optional `review.reference_files` field (src/deepwork/schemas/deepreview_schema.json), parsed with paths resolved against the rule's source dir. - `build_instruction_file` emits a new `## Reference Materials` section with fenced contents of each referenced file, honoring MAX_INLINE_FILES (20) and MAX_INLINE_TOTAL_BYTES (256KB) caps, with graceful markers for missing/unreadable files (src/deepwork/review/instructions.py). - Matcher propagates `reference_files` from ReviewRule to ReviewTask for all strategies (src/deepwork/review/matcher.py). - DeepSchema bridge now populates reference_files from examples, references, and json_schema_path, resolved relative to the schema file's directory. URL-style entries (http/https) are skipped. Missing-file errors are surfaced via the existing errors channel of generate_review_rules (src/deepwork/deepschema/review_bridge.py). - Fix a pre-existing broken relative path in the job_yml standard schema (`../../../doc/...` → `../../../../doc/...`) that the new error-surfacing flagged (src/deepwork/standard_schemas/job_yml/deepschema.yml). Tests cover inlining, count/byte caps, missing files, .deepreview parsing, and DeepSchema bridge population + error surfacing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(review): examples listed not inlined; add requirements + docs Follow-up to the review pipeline reference_files work: - DeepSchema bridge now lists `examples` (path + description) inside the generated review instructions instead of inlining their contents, keeping the prompt lean. `references` and `json_schema_path` are still inlined. - Add REVIEW-REQ-001.10 (Reference Files config), REVIEW-REQ-005.8 (Reference Materials section + caps), and DW-REQ-011.11 (Review Bridge Reference Files) covering the new end-user-observable behavior. - Add traceability comments on the new tests referencing the new REQ IDs. - Add test coverage for the new listing behavior and URL-reference skipping. - Document reference_files in README_REVIEWS.md and the deepreviews skill, including the 20-file / 256 KB caps and the examples-listed-vs-references- inlined split for DeepSchema-generated reviews. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: ruff format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1b8ac70 commit 994fb70

14 files changed

Lines changed: 484 additions & 4 deletions

File tree

README_REVIEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,19 @@ rule_name:
106106
additional_context: # Optional. Extra context for the reviewer.
107107
all_changed_filenames: true
108108
unchanged_matching_files: true
109+
reference_files: # Optional. Files whose contents are inlined
110+
- path: "docs/style_guide.md" # into a "## Reference Materials" section of
111+
description: "Style guide" # every generated review instruction file.
109112
```
110113
114+
### Reference Files
115+
116+
`reference_files` inline small support documents (style guides, JSON schemas, templates) directly into the reviewer's prompt so the agent does not need to fetch them at review time. Paths are resolved relative to the `.deepreview` file's directory.
117+
118+
Inlining is capped: at most 20 files and 256 KB of total content per review. Files beyond either cap are listed in an omitted summary line; oversized files are truncated with a marker. Missing or unreadable files produce a graceful marker but do not abort the review.
119+
120+
DeepSchema-generated reviews automatically populate `reference_files` from the schema's `references` entries and `json_schema_path`. A schema's `examples` are listed (by path and description) in the review instructions but are **not** inlined — reviewers can fetch them on demand.
121+
111122
## Review Strategies
112123

113124
The `strategy` field controls how matched files are grouped into review tasks.

plugins/claude/skills/deepreviews/SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,15 @@ rule_name:
5252
additional_context: # Optional
5353
all_changed_filenames: true # Include all changed files list
5454
unchanged_matching_files: true # Include unchanged files matching the pattern
55+
reference_files: # Optional: inlined into review prompt
56+
- path: "docs/style_guide.md"
57+
description: "Coding style guide"
5558
```
5659
60+
### Reference Files
61+
62+
`reference_files` inline small support files (style guides, schemas, templates) into a `## Reference Materials` section of every generated review instruction file. Paths are resolved relative to the `.deepreview` file's directory. Inlining is capped at 20 files and 256 KB of total content per review — entries beyond the caps are listed in an omitted-summary line; oversized files are truncated. Missing files produce a graceful marker rather than aborting the review. Use this instead of asking the reviewer to Read each file at runtime.
63+
5764
## Review Strategies
5865

5966
| Strategy | Behavior | Best for |
@@ -91,6 +98,8 @@ Deleted files are excluded — there's nothing to review.
9198

9299
DeepSchemas automatically generate synthetic review rules. When a file matches a DeepSchema with requirements, the review pipeline creates a rule that checks those requirements during `/review` and workflow quality gates. No `.deepreview` file is needed — the DeepSchema's `requirements` field drives the review.
93100

101+
Generated reviews automatically populate `reference_files` from the schema's `references` entries and `json_schema_path`, so reviewers receive that supporting context inlined. A schema's `examples` are listed (by path + description) in the review instructions but are **not** inlined; reviewers can open them on demand if needed. URL-valued references are skipped.
102+
94103
This means requirements defined in a DeepSchema are enforced in two places:
95104
- **Write-time**: validation runs when the file is written or edited
96105
- **Review-time**: a generated review rule checks compliance during `/review`

specs/deepwork/DW-REQ-011-deepschema.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,12 @@ The DeepSchema system provides rich, file-level schemas with automatic validatio
7777
2. Requirements about processes, user behavior, or context not present in files SHOULD be placed in the `instructions` section instead.
7878
3. Requirements MUST NOT restate constraints that are already enforced by the schema's `json_schema_path` or `verification_bash_command`, including syntactic validity (e.g., "must be valid JSON"), field types, allowed enum values, required fields, and structural shape.
7979
4. Requirements SHOULD focus on semantic rules, behavioral gotchas, and cross-field concerns that JSON Schema cannot express.
80+
81+
## DW-REQ-011.11: Review Bridge Reference Files
82+
83+
1. The DeepSchema review bridge MUST populate the generated `ReviewRule.reference_files` with the schema's `references` entries and the `json_schema_path` file (when set), so reviewers receive their contents inlined in the review instructions.
84+
2. Each referenced path MUST be resolved relative to the schema file's directory.
85+
3. The schema's `examples` entries MUST NOT be inlined as reference files. Instead, the bridge MUST list them in the rule's review instruction text with their `path` and `description` so reviewers know the example material exists and can fetch it on demand.
86+
4. The order of inlined reference files MUST be deterministic across runs for a given schema.
87+
5. When a referenced file cannot be located on disk, the bridge MUST skip it and surface an error through the `errors` return of `generate_review_rules`. A missing reference file MUST NOT prevent the rule from being generated.
88+
6. Reference entries whose `path` begins with `http://` or `https://` MUST be skipped without producing an error, since they are informational pointers rather than local files.

specs/deepwork/review/REVIEW-REQ-001-deepreview-config.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,9 @@ DeepWork Reviews uses `.deepreview` YAML configuration files to define review ru
8585
5. If the command fails (non-zero exit or timeout), the system MUST inject an error message into the instruction file rather than failing the pipeline.
8686
6. The command MUST be executed at most once per unique command string across all tasks in a review run.
8787
7. When multiple rules declare precompute commands, all unique commands MUST be executed in parallel.
88+
89+
### REVIEW-REQ-001.10: Reference Files
90+
91+
1. A rule's `review` block MAY declare an optional `reference_files` array. Each entry MUST be an object with a required `path` field and an optional `description` field.
92+
2. Each `reference_files.path` MUST be resolved relative to the `.deepreview` file's directory and surfaced on the parsed rule as an absolute path with its original relative string preserved as a display label.
93+
3. When `reference_files` is not specified, the parsed rule's `reference_files` MUST default to an empty list.

specs/deepwork/review/REVIEW-REQ-005-instruction-generation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,12 @@ For each `ReviewTask`, the system generates a self-contained markdown instructio
5555
2. The "Precomputed Context" section MUST be the last content section in the instruction file, appearing after all file listing sections and before the "After Review" section.
5656
3. The section MUST contain the verbatim stdout of the precomputed command.
5757
4. When the command failed, the section MUST contain an error message with stderr and exit code.
58+
59+
### REVIEW-REQ-005.8: Reference Materials Section
60+
61+
1. When a task's `reference_files` is empty, the instruction file MUST NOT contain a "Reference Materials" section.
62+
2. When a task has `reference_files`, the instruction file MUST contain a "## Reference Materials" section placed between "Review Instructions" and "Files to Review".
63+
3. Each inlined file MUST be rendered with a `### {relative_label}` subheading, the optional description, and the file contents inside a fenced code block whose language is inferred from the file extension.
64+
4. The number of inlined reference files MUST NOT exceed `MAX_INLINE_FILES` (20). Entries beyond that cap MUST be listed in an "omitted due to size/count caps" summary line rather than inlined.
65+
5. The total inlined byte size of reference file contents MUST NOT exceed `MAX_INLINE_TOTAL_BYTES` (256 * 1024). Files whose contents would exceed the remaining byte budget MUST be truncated with a visible truncation marker, and any subsequent entries MUST be reported in the omitted summary line.
66+
6. When a referenced file cannot be read (missing, permission denied, or invalid UTF-8), the system MUST emit a graceful marker line referencing the file and the error, MUST NOT abort the section, and MUST NOT count the file's would-be bytes against the budget.

src/deepwork/deepschema/review_bridge.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from deepwork.deepschema.config import DeepSchema
1313
from deepwork.deepschema.discovery import anonymous_target_filename, discover_all_schemas
1414
from deepwork.deepschema.resolver import resolve_all
15-
from deepwork.review.config import ReviewRule
15+
from deepwork.review.config import ReferenceFile, ReviewRule
1616

1717

1818
def generate_review_rules(
@@ -36,6 +36,12 @@ def generate_review_rules(
3636
for schema in resolved:
3737
rule = _schema_to_review_rule(schema, project_root)
3838
if rule is not None:
39+
refs, ref_errors = _collect_reference_files(schema)
40+
rule.reference_files = refs
41+
example_section = _build_examples_section(schema)
42+
if example_section:
43+
rule.instructions = rule.instructions.rstrip() + "\n\n" + example_section
44+
errors.extend(ref_errors)
3945
rules.append(rule)
4046

4147
return rules, errors
@@ -146,6 +152,83 @@ def _build_anonymous_instructions(schema: DeepSchema) -> str:
146152
return "".join(parts)
147153

148154

155+
def _collect_reference_files(
156+
schema: DeepSchema,
157+
) -> tuple[list[ReferenceFile], list[str]]:
158+
"""Collect ReferenceFile entries from a schema's examples, references, and JSON schema.
159+
160+
Paths are resolved relative to the schema's source directory
161+
(``schema.source_path.parent``), matching the convention used by
162+
existing schema YAML files (e.g. ``json_schema_path: "job.schema.json"``
163+
in a sibling directory).
164+
165+
Missing files are skipped and an error string is appended to the
166+
returned error list so callers can surface it to the user.
167+
"""
168+
schema_dir = schema.source_path.parent
169+
refs: list[ReferenceFile] = []
170+
errors: list[str] = []
171+
172+
def _add(raw_path: str, description: str | None, source_field: str) -> None:
173+
# Skip URL-style references — these are informational pointers, not
174+
# local files to inline.
175+
if raw_path.startswith(("http://", "https://")):
176+
return
177+
resolved = (schema_dir / raw_path).resolve()
178+
if not resolved.exists():
179+
errors.append(
180+
f"{schema.source_path}: {source_field} entry '{raw_path}' not found "
181+
f"(resolved to {resolved})"
182+
)
183+
return
184+
refs.append(
185+
ReferenceFile(
186+
path=resolved,
187+
relative_label=raw_path,
188+
description=description,
189+
)
190+
)
191+
192+
# Note: schema.examples are intentionally NOT inlined — they are listed
193+
# in the instructions via _build_examples_section so reviewers know they
194+
# exist without bloating the prompt.
195+
for reference in schema.references:
196+
path = reference.get("path")
197+
if path:
198+
_add(path, reference.get("description"), "references")
199+
200+
if schema.json_schema_path:
201+
_add(
202+
schema.json_schema_path,
203+
f"JSON Schema for {schema.name}",
204+
"json_schema_path",
205+
)
206+
207+
return refs, errors
208+
209+
210+
def _build_examples_section(schema: DeepSchema) -> str:
211+
"""Build a text section listing the schema's example files by path + description.
212+
213+
Examples are listed (not inlined) so the reviewer knows what reference
214+
material exists without bloating the prompt.
215+
"""
216+
if not schema.examples:
217+
return ""
218+
219+
lines = ["Example files available for reference (read on demand):"]
220+
for example in schema.examples:
221+
path = example.get("path")
222+
if not path:
223+
continue
224+
description = example.get("description") or ""
225+
if description:
226+
lines.append(f"- `{path}` — {description}")
227+
else:
228+
lines.append(f"- `{path}`")
229+
return "\n".join(lines)
230+
231+
149232
def _build_requirements_body(requirements: dict[str, str]) -> str:
150233
"""Build the RFC 2119 requirements review section."""
151234
req_lines = "\n".join(f"- **{name}**: {desc}" for name, desc in requirements.items())

src/deepwork/review/config.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ class ConfigError(Exception):
2020
pass
2121

2222

23+
@dataclass
24+
class ReferenceFile:
25+
"""A file whose contents should be inlined into a review's instructions."""
26+
27+
path: Path # Absolute path on disk
28+
relative_label: str # Display label (e.g. the original relative path from source)
29+
description: str | None = None
30+
31+
2332
@dataclass
2433
class ReviewRule:
2534
"""A single named review rule from a .deepreview file."""
@@ -37,6 +46,7 @@ class ReviewRule:
3746
source_dir: Path # Directory containing the .deepreview file
3847
source_file: Path # Path to the .deepreview file
3948
source_line: int # Line number of the rule name in the .deepreview file
49+
reference_files: list[ReferenceFile] = field(default_factory=list)
4050

4151

4252
@dataclass
@@ -52,6 +62,7 @@ class ReviewTask:
5262
all_changed_filenames: list[str] | None = None
5363
precomputed_info_bash_command: str | None = None # Resolved command to run
5464
inline_content: str | None = None # Inline string value for type: string outputs
65+
reference_files: list[ReferenceFile] = field(default_factory=list)
5566

5667

5768
def parse_deepreview_file(filepath: Path) -> list[ReviewRule]:
@@ -135,6 +146,8 @@ def _parse_rule(
135146
review_data.get("precomputed_info_for_reviewer_bash_command"), source_dir
136147
)
137148

149+
reference_files = _parse_reference_files(review_data.get("reference_files", []), source_dir)
150+
138151
return ReviewRule(
139152
name=name,
140153
description=description,
@@ -149,9 +162,30 @@ def _parse_rule(
149162
source_dir=source_dir,
150163
source_file=source_file,
151164
source_line=source_line,
165+
reference_files=reference_files,
152166
)
153167

154168

169+
def _parse_reference_files(entries: list[dict[str, Any]], source_dir: Path) -> list[ReferenceFile]:
170+
"""Parse optional `reference_files` entries from a .deepreview rule.
171+
172+
Paths are resolved relative to ``source_dir``. Existence is not checked
173+
here — missing files are handled at inlining time so authors see the
174+
error surfaced in the generated review instructions.
175+
"""
176+
result: list[ReferenceFile] = []
177+
for entry in entries:
178+
raw_path = entry["path"]
179+
result.append(
180+
ReferenceFile(
181+
path=(source_dir / raw_path).resolve(),
182+
relative_label=raw_path,
183+
description=entry.get("description"),
184+
)
185+
)
186+
return result
187+
188+
155189
def _resolve_instructions(instructions: str | dict[str, Any], source_dir: Path) -> str:
156190
"""Resolve instruction text — either inline string or file reference.
157191

src/deepwork/review/instructions.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,25 @@
1111
from concurrent.futures import ThreadPoolExecutor, as_completed
1212
from pathlib import Path
1313

14-
from deepwork.review.config import ReviewTask
14+
from deepwork.review.config import ReferenceFile, ReviewTask
1515
from deepwork.utils.fs import safe_write
1616

1717
INSTRUCTIONS_DIR = ".deepwork/tmp/review_instructions"
1818

19+
# Caps on inlined reference file content to keep review prompts tractable.
20+
MAX_INLINE_FILES = 20
21+
MAX_INLINE_TOTAL_BYTES = 256 * 1024
22+
23+
_FENCE_LANG_BY_EXT = {
24+
".yml": "yaml",
25+
".yaml": "yaml",
26+
".json": "json",
27+
".md": "markdown",
28+
".py": "python",
29+
".sh": "bash",
30+
".toml": "toml",
31+
}
32+
1933
_SANITIZE_RE = re.compile(r"[^a-zA-Z0-9\-_.]")
2034

2135

@@ -232,6 +246,12 @@ def build_instruction_file(
232246
parts.append(task.instructions.strip())
233247
parts.append("")
234248

249+
# Reference materials — inlined file contents (subject to caps)
250+
if task.reference_files:
251+
parts.append("## Reference Materials\n")
252+
parts.append(_build_reference_files_section(task.reference_files))
253+
parts.append("")
254+
235255
# Files to review (omitted for inline-content tasks with no files)
236256
if task.files_to_review:
237257
parts.append("## Files to Review\n")
@@ -291,6 +311,65 @@ def build_instruction_file(
291311
return "\n".join(parts)
292312

293313

314+
def _build_reference_files_section(reference_files: list[ReferenceFile]) -> str:
315+
"""Build a markdown section inlining reference file contents.
316+
317+
Reads each file in order and emits a `### {label}` subsection with an
318+
optional description and a fenced code block. Honors ``MAX_INLINE_FILES``
319+
and ``MAX_INLINE_TOTAL_BYTES`` — once either cap is hit, remaining
320+
entries are summarized in an "omitted" line rather than inlined.
321+
322+
Files that cannot be read produce a graceful marker but do not abort the
323+
section (and their would-be bytes do not count toward the budget).
324+
"""
325+
parts: list[str] = []
326+
total_bytes = 0
327+
inlined_count = 0
328+
omitted: list[str] = []
329+
330+
for ref in reference_files:
331+
if inlined_count >= MAX_INLINE_FILES or total_bytes >= MAX_INLINE_TOTAL_BYTES:
332+
omitted.append(ref.relative_label)
333+
continue
334+
335+
header = f"### {ref.relative_label}"
336+
if ref.description:
337+
header += f"\n\n{ref.description.strip()}"
338+
339+
try:
340+
content = ref.path.read_text(encoding="utf-8")
341+
except (OSError, UnicodeDecodeError) as e:
342+
parts.append(header)
343+
parts.append(f"\n\n(could not inline {ref.relative_label}: {e})\n")
344+
continue
345+
346+
lang = _FENCE_LANG_BY_EXT.get(ref.path.suffix.lower(), "text")
347+
remaining = MAX_INLINE_TOTAL_BYTES - total_bytes
348+
truncated_marker = ""
349+
content_bytes = content.encode("utf-8")
350+
if len(content_bytes) > remaining:
351+
# Truncate at a character boundary near the byte budget.
352+
content = content_bytes[:remaining].decode("utf-8", errors="ignore")
353+
truncated_marker = (
354+
f"\n... (truncated: file is {len(content_bytes)} bytes, "
355+
f"budget left was {remaining})"
356+
)
357+
358+
parts.append(header)
359+
parts.append(f"\n\n```{lang}\n{content}{truncated_marker}\n```\n")
360+
total_bytes += len(content.encode("utf-8"))
361+
inlined_count += 1
362+
363+
if omitted:
364+
omitted_list = ", ".join(omitted)
365+
parts.append(
366+
f"\n_({len(omitted)} more reference file(s) omitted due to size/count caps: "
367+
f"{omitted_list})_\n"
368+
)
369+
370+
return "".join(parts)
371+
372+
294373
def _describe_scope(task: ReviewTask) -> str:
295374
"""Generate a human-readable scope description for the task.
296375

src/deepwork/review/matcher.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def match_files_to_rules(
236236
source_location=source_location,
237237
all_changed_filenames=all_filenames,
238238
precomputed_info_bash_command=precompute_cmd,
239+
reference_files=rule.reference_files,
239240
)
240241
)
241242

@@ -253,6 +254,7 @@ def match_files_to_rules(
253254
additional_files=additional,
254255
all_changed_filenames=all_filenames,
255256
precomputed_info_bash_command=precompute_cmd,
257+
reference_files=rule.reference_files,
256258
)
257259
)
258260

@@ -266,6 +268,7 @@ def match_files_to_rules(
266268
source_location=source_location,
267269
all_changed_filenames=all_filenames,
268270
precomputed_info_bash_command=precompute_cmd,
271+
reference_files=rule.reference_files,
269272
)
270273
)
271274

0 commit comments

Comments
 (0)