Skip to content

Commit 0e1ade7

Browse files
fix: make planning-doc validation portable across knowledge roots
Closes #212
1 parent 5d0ee15 commit 0e1ade7

2 files changed

Lines changed: 104 additions & 2 deletions

File tree

scripts/validate_planning_docs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def validate_doc_registry(repo_root: Path) -> list[str]:
278278
return violations
279279

280280
for template_path in sorted((root / "tasks" / "templates").glob("*.md")):
281-
repo_path = _repo_path(root.parent, template_path)
281+
repo_path = _corpus_path(root, template_path)
282282
record = registry.by_path.get(repo_path)
283283
if record is None:
284284
violations.append(f"{repo_path}: missing from document registry")
@@ -298,7 +298,7 @@ def validate_task_templates(repo_root: Path) -> list[str]:
298298

299299
seen_titles: dict[str, str] = {}
300300
for template_path in template_paths:
301-
repo_path = _repo_path(repo_root, template_path)
301+
repo_path = _corpus_path(root, template_path)
302302
text = template_path.read_text(encoding="utf-8", errors="replace")
303303
raw, body = sop_parse.split_frontmatter(text)
304304
metadata = sop_parse.parse_frontmatter(raw) if raw else {}
@@ -482,6 +482,10 @@ def _normalize_text(text: str) -> str:
482482
return text.lower()
483483

484484

485+
def _corpus_path(content_root: Path, path: Path) -> str:
486+
return (Path("content") / path.resolve().relative_to(content_root.resolve())).as_posix()
487+
488+
485489
def _repo_path(repo_root: Path, path: Path) -> str:
486490
return path.resolve().relative_to(repo_root).as_posix()
487491

tests/planning_docs/test_validate_planning_docs.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,52 @@
1313
import validate_planning_docs # noqa: E402
1414

1515

16+
VALID_TEMPLATE = """---
17+
title: "Newsletter"
18+
doc_type: task-template
19+
schema_version: 1
20+
source: "backend/scripts/seed-templates.ts"
21+
systems:
22+
- dataops
23+
- datatasks
24+
tags:
25+
- task-template
26+
- newsletter
27+
---
28+
29+
# Newsletter
30+
31+
<!-- sop-section-start: summary -->
32+
## Summary
33+
<!-- sop-section-start: purpose -->
34+
## Purpose
35+
<!-- sop-section-start: references -->
36+
## References
37+
<!-- sop-section-start: required-card-links -->
38+
## Required Card Links
39+
<!-- sop-section-start: task-definitions -->
40+
## Task Definitions
41+
42+
| # | Ref ID | Phase | Offset | Owner | Operator action | Context | Proof / closure | Waiting / follow-up |
43+
| - | - | - | -: | - | - | - | - | - |
44+
| 1 | `create-document` | intake | -14 | owner | Create document | doc.id | url: Document | |
45+
"""
46+
47+
INVALID_TEMPLATE = """---
48+
title: "Invalid"
49+
doc_type: template
50+
schema_version: 2
51+
source: "wrong-place.ts"
52+
systems:
53+
- dataops
54+
tags:
55+
- task-template
56+
---
57+
58+
# Invalid
59+
"""
60+
61+
1662
def test_current_repo_planning_docs_contract_passes():
1763
assert validate_planning_docs.validate(REPO_ROOT) == []
1864

@@ -121,3 +167,55 @@ def test_task_template_accepts_richer_operator_workflow_table(tmp_path):
121167
violations = validate_planning_docs.validate_task_templates(tmp_path)
122168

123169
assert violations == []
170+
171+
172+
def test_task_template_paths_are_stable_across_corpus_layouts(tmp_path, monkeypatch):
173+
layouts = {
174+
"repository-local": (
175+
tmp_path / "repository-local" / "app" / "content",
176+
tmp_path / "repository-local" / "app",
177+
),
178+
".knowledge": (
179+
tmp_path / "dot-knowledge" / "app" / ".knowledge" / "content",
180+
tmp_path / "dot-knowledge" / "app",
181+
),
182+
"configured-external": (
183+
tmp_path / "configured-external" / "corpus" / "content",
184+
tmp_path / "configured-external" / "app",
185+
),
186+
"sibling-style": (
187+
tmp_path / "sibling-style" / "dataops-knowledge" / "content",
188+
tmp_path / "sibling-style" / "differently-named-app",
189+
),
190+
}
191+
results = {}
192+
193+
for layout, (content_root, app_root) in layouts.items():
194+
templates_dir = content_root / "tasks" / "templates"
195+
templates_dir.mkdir(parents=True)
196+
(templates_dir / "newsletter.md").write_text(VALID_TEMPLATE, encoding="utf-8")
197+
198+
if layout == "configured-external":
199+
monkeypatch.setenv("DATAOPS_CONTENT_ROOT", str(content_root))
200+
else:
201+
monkeypatch.delenv("DATAOPS_CONTENT_ROOT", raising=False)
202+
203+
assert validate_planning_docs.validate_task_templates(app_root) == []
204+
205+
newsletter_path = templates_dir / "newsletter.md"
206+
newsletter_path.write_text(
207+
INVALID_TEMPLATE.replace('title: "Invalid"', 'title: "Newsletter"'),
208+
encoding="utf-8",
209+
)
210+
violations = validate_planning_docs.validate_task_templates(app_root)
211+
212+
assert violations
213+
assert all(violation.startswith("content/tasks/templates/") for violation in violations)
214+
assert not any(str(app_root) in violation for violation in violations)
215+
assert any(
216+
violation == "content/tasks/templates/newsletter.md: doc_type must be task-template"
217+
for violation in violations
218+
)
219+
results[layout] = tuple(violations)
220+
221+
assert len(set(results.values())) == 1

0 commit comments

Comments
 (0)