Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README_REVIEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,19 @@ rule_name:
additional_context: # Optional. Extra context for the reviewer.
all_changed_filenames: true
unchanged_matching_files: true
reference_files: # Optional. Files whose contents are inlined
- path: "docs/style_guide.md" # into a "## Reference Materials" section of
description: "Style guide" # every generated review instruction file.
```

### Reference Files

`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.

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.

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.

## Review Strategies

The `strategy` field controls how matched files are grouped into review tasks.
Expand Down
9 changes: 9 additions & 0 deletions plugins/claude/skills/deepreviews/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,15 @@ rule_name:
additional_context: # Optional
all_changed_filenames: true # Include all changed files list
unchanged_matching_files: true # Include unchanged files matching the pattern
reference_files: # Optional: inlined into review prompt
- path: "docs/style_guide.md"
description: "Coding style guide"
```

### Reference Files

`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.

## Review Strategies

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

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.

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.

This means requirements defined in a DeepSchema are enforced in two places:
- **Write-time**: validation runs when the file is written or edited
- **Review-time**: a generated review rule checks compliance during `/review`
Expand Down
9 changes: 9 additions & 0 deletions specs/deepwork/DW-REQ-011-deepschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,12 @@ The DeepSchema system provides rich, file-level schemas with automatic validatio
2. Requirements about processes, user behavior, or context not present in files SHOULD be placed in the `instructions` section instead.
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.
4. Requirements SHOULD focus on semantic rules, behavioral gotchas, and cross-field concerns that JSON Schema cannot express.

## DW-REQ-011.11: Review Bridge Reference Files

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.
2. Each referenced path MUST be resolved relative to the schema file's directory.
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.
4. The order of inlined reference files MUST be deterministic across runs for a given schema.
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.
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.
6 changes: 6 additions & 0 deletions specs/deepwork/review/REVIEW-REQ-001-deepreview-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,9 @@ DeepWork Reviews uses `.deepreview` YAML configuration files to define review ru
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.
6. The command MUST be executed at most once per unique command string across all tasks in a review run.
7. When multiple rules declare precompute commands, all unique commands MUST be executed in parallel.

### REVIEW-REQ-001.10: Reference Files

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.
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.
3. When `reference_files` is not specified, the parsed rule's `reference_files` MUST default to an empty list.
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,12 @@ For each `ReviewTask`, the system generates a self-contained markdown instructio
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.
3. The section MUST contain the verbatim stdout of the precomputed command.
4. When the command failed, the section MUST contain an error message with stderr and exit code.

### REVIEW-REQ-005.8: Reference Materials Section

1. When a task's `reference_files` is empty, the instruction file MUST NOT contain a "Reference Materials" section.
2. When a task has `reference_files`, the instruction file MUST contain a "## Reference Materials" section placed between "Review Instructions" and "Files to Review".
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.
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.
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.
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.
85 changes: 84 additions & 1 deletion src/deepwork/deepschema/review_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from deepwork.deepschema.config import DeepSchema
from deepwork.deepschema.discovery import anonymous_target_filename, discover_all_schemas
from deepwork.deepschema.resolver import resolve_all
from deepwork.review.config import ReviewRule
from deepwork.review.config import ReferenceFile, ReviewRule


def generate_review_rules(
Expand All @@ -36,6 +36,12 @@ def generate_review_rules(
for schema in resolved:
rule = _schema_to_review_rule(schema, project_root)
if rule is not None:
refs, ref_errors = _collect_reference_files(schema)
rule.reference_files = refs
example_section = _build_examples_section(schema)
if example_section:
rule.instructions = rule.instructions.rstrip() + "\n\n" + example_section
errors.extend(ref_errors)
rules.append(rule)

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


def _collect_reference_files(
schema: DeepSchema,
) -> tuple[list[ReferenceFile], list[str]]:
"""Collect ReferenceFile entries from a schema's examples, references, and JSON schema.

Paths are resolved relative to the schema's source directory
(``schema.source_path.parent``), matching the convention used by
existing schema YAML files (e.g. ``json_schema_path: "job.schema.json"``
in a sibling directory).

Missing files are skipped and an error string is appended to the
returned error list so callers can surface it to the user.
"""
schema_dir = schema.source_path.parent
refs: list[ReferenceFile] = []
errors: list[str] = []

def _add(raw_path: str, description: str | None, source_field: str) -> None:
# Skip URL-style references — these are informational pointers, not
# local files to inline.
if raw_path.startswith(("http://", "https://")):
return
resolved = (schema_dir / raw_path).resolve()
if not resolved.exists():
errors.append(
f"{schema.source_path}: {source_field} entry '{raw_path}' not found "
f"(resolved to {resolved})"
)
return
refs.append(
ReferenceFile(
path=resolved,
relative_label=raw_path,
description=description,
)
)

# Note: schema.examples are intentionally NOT inlined — they are listed
# in the instructions via _build_examples_section so reviewers know they
# exist without bloating the prompt.
for reference in schema.references:
path = reference.get("path")
if path:
_add(path, reference.get("description"), "references")

if schema.json_schema_path:
_add(
schema.json_schema_path,
f"JSON Schema for {schema.name}",
"json_schema_path",
)

return refs, errors


def _build_examples_section(schema: DeepSchema) -> str:
"""Build a text section listing the schema's example files by path + description.

Examples are listed (not inlined) so the reviewer knows what reference
material exists without bloating the prompt.
"""
if not schema.examples:
return ""

lines = ["Example files available for reference (read on demand):"]
for example in schema.examples:
path = example.get("path")
if not path:
continue
description = example.get("description") or ""
if description:
lines.append(f"- `{path}` — {description}")
else:
lines.append(f"- `{path}`")
return "\n".join(lines)


def _build_requirements_body(requirements: dict[str, str]) -> str:
"""Build the RFC 2119 requirements review section."""
req_lines = "\n".join(f"- **{name}**: {desc}" for name, desc in requirements.items())
Expand Down
34 changes: 34 additions & 0 deletions src/deepwork/review/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ class ConfigError(Exception):
pass


@dataclass
class ReferenceFile:
"""A file whose contents should be inlined into a review's instructions."""

path: Path # Absolute path on disk
relative_label: str # Display label (e.g. the original relative path from source)
description: str | None = None


@dataclass
class ReviewRule:
"""A single named review rule from a .deepreview file."""
Expand All @@ -37,6 +46,7 @@ class ReviewRule:
source_dir: Path # Directory containing the .deepreview file
source_file: Path # Path to the .deepreview file
source_line: int # Line number of the rule name in the .deepreview file
reference_files: list[ReferenceFile] = field(default_factory=list)


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


def parse_deepreview_file(filepath: Path) -> list[ReviewRule]:
Expand Down Expand Up @@ -135,6 +146,8 @@ def _parse_rule(
review_data.get("precomputed_info_for_reviewer_bash_command"), source_dir
)

reference_files = _parse_reference_files(review_data.get("reference_files", []), source_dir)

return ReviewRule(
name=name,
description=description,
Expand All @@ -149,9 +162,30 @@ def _parse_rule(
source_dir=source_dir,
source_file=source_file,
source_line=source_line,
reference_files=reference_files,
)


def _parse_reference_files(entries: list[dict[str, Any]], source_dir: Path) -> list[ReferenceFile]:
"""Parse optional `reference_files` entries from a .deepreview rule.

Paths are resolved relative to ``source_dir``. Existence is not checked
here — missing files are handled at inlining time so authors see the
error surfaced in the generated review instructions.
"""
result: list[ReferenceFile] = []
for entry in entries:
raw_path = entry["path"]
result.append(
ReferenceFile(
path=(source_dir / raw_path).resolve(),
relative_label=raw_path,
description=entry.get("description"),
)
)
return result


def _resolve_instructions(instructions: str | dict[str, Any], source_dir: Path) -> str:
"""Resolve instruction text — either inline string or file reference.

Expand Down
81 changes: 80 additions & 1 deletion src/deepwork/review/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

from deepwork.review.config import ReviewTask
from deepwork.review.config import ReferenceFile, ReviewTask
from deepwork.utils.fs import safe_write

INSTRUCTIONS_DIR = ".deepwork/tmp/review_instructions"

# Caps on inlined reference file content to keep review prompts tractable.
MAX_INLINE_FILES = 20
MAX_INLINE_TOTAL_BYTES = 256 * 1024

_FENCE_LANG_BY_EXT = {
".yml": "yaml",
".yaml": "yaml",
".json": "json",
".md": "markdown",
".py": "python",
".sh": "bash",
".toml": "toml",
}

_SANITIZE_RE = re.compile(r"[^a-zA-Z0-9\-_.]")


Expand Down Expand Up @@ -232,6 +246,12 @@ def build_instruction_file(
parts.append(task.instructions.strip())
parts.append("")

# Reference materials — inlined file contents (subject to caps)
if task.reference_files:
parts.append("## Reference Materials\n")
parts.append(_build_reference_files_section(task.reference_files))
parts.append("")

# Files to review (omitted for inline-content tasks with no files)
if task.files_to_review:
parts.append("## Files to Review\n")
Expand Down Expand Up @@ -291,6 +311,65 @@ def build_instruction_file(
return "\n".join(parts)


def _build_reference_files_section(reference_files: list[ReferenceFile]) -> str:
"""Build a markdown section inlining reference file contents.

Reads each file in order and emits a `### {label}` subsection with an
optional description and a fenced code block. Honors ``MAX_INLINE_FILES``
and ``MAX_INLINE_TOTAL_BYTES`` — once either cap is hit, remaining
entries are summarized in an "omitted" line rather than inlined.

Files that cannot be read produce a graceful marker but do not abort the
section (and their would-be bytes do not count toward the budget).
"""
parts: list[str] = []
total_bytes = 0
inlined_count = 0
omitted: list[str] = []

for ref in reference_files:
if inlined_count >= MAX_INLINE_FILES or total_bytes >= MAX_INLINE_TOTAL_BYTES:
omitted.append(ref.relative_label)
continue

header = f"### {ref.relative_label}"
if ref.description:
header += f"\n\n{ref.description.strip()}"

try:
content = ref.path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as e:
parts.append(header)
parts.append(f"\n\n(could not inline {ref.relative_label}: {e})\n")
continue

lang = _FENCE_LANG_BY_EXT.get(ref.path.suffix.lower(), "text")
remaining = MAX_INLINE_TOTAL_BYTES - total_bytes
truncated_marker = ""
content_bytes = content.encode("utf-8")
if len(content_bytes) > remaining:
# Truncate at a character boundary near the byte budget.
content = content_bytes[:remaining].decode("utf-8", errors="ignore")
truncated_marker = (
f"\n... (truncated: file is {len(content_bytes)} bytes, "
f"budget left was {remaining})"
)

parts.append(header)
parts.append(f"\n\n```{lang}\n{content}{truncated_marker}\n```\n")
total_bytes += len(content.encode("utf-8"))
inlined_count += 1

if omitted:
omitted_list = ", ".join(omitted)
parts.append(
f"\n_({len(omitted)} more reference file(s) omitted due to size/count caps: "
f"{omitted_list})_\n"
)

return "".join(parts)


def _describe_scope(task: ReviewTask) -> str:
"""Generate a human-readable scope description for the task.

Expand Down
3 changes: 3 additions & 0 deletions src/deepwork/review/matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def match_files_to_rules(
source_location=source_location,
all_changed_filenames=all_filenames,
precomputed_info_bash_command=precompute_cmd,
reference_files=rule.reference_files,
)
)

Expand All @@ -253,6 +254,7 @@ def match_files_to_rules(
additional_files=additional,
all_changed_filenames=all_filenames,
precomputed_info_bash_command=precompute_cmd,
reference_files=rule.reference_files,
)
)

Expand All @@ -266,6 +268,7 @@ def match_files_to_rules(
source_location=source_location,
all_changed_filenames=all_filenames,
precomputed_info_bash_command=precompute_cmd,
reference_files=rule.reference_files,
)
)

Expand Down
Loading
Loading