fix(example): skip prompt writes when output exists, add quality rubrics

INFRA-TASKS #5 — process_chapters.py now skips writing *-prompt.md files
when the corresponding output file already exists on disk. DB-only rebuilds
no longer dirty the working tree with unchanged prompt content.

INFRA-TASKS #8 — Added '## Quality Metrics' section to the entity and VSM
mapping schemas, defining the five evaluation dimensions (Definition Precision,
Source Grounding, Domain Placement, VSM Relevance, Explanatory Value) with
1–5 rubrics used by the evaluate-entity template.

Also updated INFRA-TASKS.md to reflect current resolution status for tasks
4–19 across S2 and S3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 06:04:09 +01:00
parent dfab3d598b
commit fa27572f43
4 changed files with 258 additions and 39 deletions

View File

@@ -487,14 +487,16 @@ class ChapterProcessor:
if not prompt:
return None
# Write compiled prompt for inspection
prompt_file = self._entities_dir() / f"{chapter_id}-prompt.md"
prompt_file.parent.mkdir(parents=True, exist_ok=True)
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
view_file = self._entities_dir() / f"{chapter_id}-entities.md"
# Write compiled prompt only when no output exists yet (avoids dirty
# working tree on DB-only rebuilds — Task 5 fix)
prompt_file = self._entities_dir() / f"{chapter_id}-prompt.md"
if not (view_file.exists() and "{{ include" in view_file.read_text()):
prompt_file.parent.mkdir(parents=True, exist_ok=True)
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
# ── PRIMARY: chapter view with transclusion already on disk ──
if view_file.exists() and "{{ include" in view_file.read_text():
content, entity_files = self._read_entities_from_view(chapter_id)
@@ -575,11 +577,14 @@ class ChapterProcessor:
if not prompt:
return None
prompt_file = self.example_dir / "output" / "mappings" / f"{chapter_id}-prompt.md"
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
output_file = self.example_dir / "output" / "mappings" / f"{chapter_id}-mappings.md"
# Write compiled prompt only when output does not yet exist (Task 5 fix)
if not output_file.exists():
prompt_file = self.example_dir / "output" / "mappings" / f"{chapter_id}-prompt.md"
prompt_file.parent.mkdir(parents=True, exist_ok=True)
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
if output_file.exists():
content = output_file.read_text()
self.store_output_artifact(
@@ -622,11 +627,14 @@ class ChapterProcessor:
if not prompt:
return None
prompt_file = self.example_dir / "output" / "analyses" / f"{chapter_id}-prompt.md"
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
output_file = self.example_dir / "output" / "analyses" / f"{chapter_id}-analysis.md"
# Write compiled prompt only when output does not yet exist (Task 5 fix)
if not output_file.exists():
prompt_file = self.example_dir / "output" / "analyses" / f"{chapter_id}-prompt.md"
prompt_file.parent.mkdir(parents=True, exist_ok=True)
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
if output_file.exists():
content = output_file.read_text()
self.store_output_artifact(
@@ -679,11 +687,14 @@ class ChapterProcessor:
if not prompt:
return None
prompt_file = self.example_dir / "output" / "metrics" / "metrics-prompt.md"
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
output_file = self.example_dir / "output" / "metrics" / "metrics-report.md"
# Write compiled prompt only when output does not yet exist (Task 5 fix)
if not output_file.exists():
prompt_file = self.example_dir / "output" / "metrics" / "metrics-prompt.md"
prompt_file.parent.mkdir(parents=True, exist_ok=True)
prompt_file.write_text(prompt)
print(f" Prompt written to {prompt_file.relative_to(self.example_dir)}")
if output_file.exists():
content = output_file.read_text()
self.store_output_artifact(
@@ -709,6 +720,123 @@ class ChapterProcessor:
print(f" Awaiting output at: {output_file.relative_to(self.example_dir)}")
return None
# ── Entity Evaluation (Task 9) ────────────────────────────────────
def _extract_quality_rubric(self) -> str:
"""Extract the Quality Metrics section from the entity schema file."""
schema_file = self.example_dir / "schemas" / "economic-entity-schema-v1.0.md"
text = schema_file.read_text()
# Find the ## Quality Metrics section up to the next ## section
import re as _re
m = _re.search(
r"^## Quality Metrics\n(.*?)^## ",
text,
flags=_re.MULTILINE | _re.DOTALL,
)
if m:
return ("## Quality Metrics\n" + m.group(1)).strip()
return text # fallback: whole schema
def _extract_source_chapter_from_entity(self, entity_text: str) -> str:
"""Extract the Source Chapter field from an entity markdown file."""
import re as _re
m = _re.search(
r"^## Source Chapter\s*\n+(.+?)(?:\n\n|\n##|\Z)",
entity_text,
flags=_re.MULTILINE | _re.DOTALL,
)
if m:
return m.group(1).strip()
return "Unknown chapter"
def evaluate_entities(self, chapter_id: Optional[str] = None) -> None:
"""Evaluate canonical entities using the evaluate-entity template.
If *chapter_id* is given, evaluates only entities introduced by that
chapter (determined from the chapter view file). Otherwise evaluates
all canonical entities.
Outputs are written to ``output/evaluations/<slug>-eval.md``.
Existing evaluation files are skipped (idempotent).
"""
evaluations_dir = self.example_dir / "output" / "evaluations"
evaluations_dir.mkdir(parents=True, exist_ok=True)
# Determine which entity files to evaluate
if chapter_id:
view_file = self._entities_dir() / f"{chapter_id}-entities.md"
if not view_file.exists():
print(f" No chapter view found for {chapter_id}")
return
_, entity_files = self._read_entities_from_view(chapter_id)
if not entity_files:
print(f" No entities found for chapter {chapter_id}")
return
print(f"Evaluating {len(entity_files)} entities from {chapter_id}...")
else:
slugs = self._list_existing_entity_names()
entity_files = [(s, self._entities_dir() / f"{s}.md") for s in slugs]
print(f"Evaluating {len(entity_files)} canonical entities...")
if not entity_files:
print(" No entities to evaluate.")
return
# Shared context loaded once
quality_rubric = self._extract_quality_rubric()
self.bind_macro_artifact(self.spaces["guidelines"], "quality_rubric", quality_rubric)
done = 0
skipped = 0
failed = 0
for slug, entity_path in entity_files:
output_file = evaluations_dir / f"{slug}-eval.md"
if output_file.exists():
skipped += 1
continue
if not entity_path.exists():
print(f" MISSING: {entity_path.name}")
failed += 1
continue
entity_text = entity_path.read_text()
source_chapter = self._extract_source_chapter_from_entity(entity_text)
# Bind per-entity macros
self.bind_macro_artifact(self.spaces["entities"], "entity_content", entity_text)
self.bind_macro_artifact(self.spaces["sources"], "source_chapter", source_chapter)
prompt = self.resolve_and_compile(
"evaluate-entity",
["entities", "sources", "vsm-reference", "guidelines"],
)
if not prompt:
print(f" FAILED to compile prompt for {slug}")
failed += 1
continue
# Write prompt only when output does not yet exist (Task 5 fix)
prompt_file = evaluations_dir / f"{slug}-eval-prompt.md"
if not output_file.exists():
prompt_file.write_text(prompt)
if not self.llm_adapter:
print(f" {slug}: prompt written, awaiting manual evaluation")
done += 1
continue
print(f" Evaluating: {slug}...")
content = self._execute_llm(prompt, output_file, f"eval:{slug}", max_tokens=1024)
if content:
done += 1
else:
failed += 1
total = done + skipped + failed
print(f"\nEvaluation complete: {done} done, {skipped} skipped (existing), {failed} failed — {total} total")
# ── Chapter Processing ───────────────────────────────────────────
def process_chapter(self, chapter_id: str, auto_commit: bool = True):
@@ -994,9 +1122,13 @@ def main():
help="Run collection-level quality checks (C1-C5)")
group.add_argument("--infospace-viability", action="store_true",
help="Show viability dashboard")
group.add_argument("--evaluate", action="store_true",
help="Evaluate entity quality using the evaluate-entity template")
parser.add_argument("--reason", type=str, default=None,
help="Reason for archiving (used with --archive-entity)")
parser.add_argument("--eval-chapter", type=str, default=None, metavar="CHAPTER_ID",
help="Limit --evaluate to entities from a specific chapter")
parser.add_argument("--no-commit", action="store_true", help="Skip git commits")
parser.add_argument(
"--provider",
@@ -1064,6 +1196,9 @@ def main():
elif args.infospace_viability:
_run_infospace_viability(example_dir)
return
elif args.evaluate:
processor.evaluate_entities(chapter_id=args.eval_chapter)
return
processor.show_stats()