Skip to content

Commit 7c8292e

Browse files
calvingilesclaude
andauthored
feat: Enable DSL validation for project specs (#39)
This change makes the project dogfood the DSL validator by: 1. Adding new builtin module types: - SpecificationModule: For spec files like markdown-link-validator.md - PrinciplesModule: For principles.md 2. Restructuring specs to pass validation: - Removed duplicate job catalog files (specs/jobs/*.md) - Removed broken job references from spec files - Added "Addresses" sections to Requirements linking to Jobs 3. Fixed bugs in DSL validator: - Fixed reference_resolver.py using target_module instead of target_type - Added "addresses" keyword detection in reference_extractor.py - Added metadata location support for identifier extraction - Imported MarkdownDocument type in validator.py 4. Added DSL validation to CI workflow All specs now pass DSL validation with 0 errors. The only remaining messages are 3 info messages about future/ docs not matching any type, which is expected. Validation results: - Documents validated: 13 - References validated: 6 - Errors: 0 - Warnings: 0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent b10df37 commit 7c8292e

15 files changed

+258
-603
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ jobs:
7474

7575
- name: Validate spec schema
7676
run: uv run spec-check check-schema --verbose
77+
78+
- name: Validate DSL structure
79+
run: uv run spec-check validate-dsl --builtin-types specs/

spec_check/dsl/builtin_types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
AcceptanceCriterion,
1717
ArchitectureDecisionModule,
1818
JobModule,
19+
PrinciplesModule,
1920
RequirementModule,
21+
SpecificationModule,
2022
)
2123

2224
# ============================================================================
@@ -28,6 +30,8 @@
2830
"Job": JobModule(),
2931
"Requirement": RequirementModule(),
3032
"ADR": ArchitectureDecisionModule(),
33+
"Specification": SpecificationModule(),
34+
"Principles": PrinciplesModule(),
3135
}
3236

3337
# Export built-in class types
@@ -41,5 +45,7 @@
4145
"JobModule",
4246
"RequirementModule",
4347
"ArchitectureDecisionModule",
48+
"SpecificationModule",
49+
"PrinciplesModule",
4450
"AcceptanceCriterion",
4551
]

spec_check/dsl/layers.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,77 @@ class ArchitectureDecisionModule(SpecModule):
213213
]
214214

215215

216+
class PrinciplesModule(SpecModule):
217+
"""
218+
Principles Document.
219+
220+
Documents architectural principles, design philosophy, and guiding
221+
tenets for the project. Typically one per project.
222+
223+
Example filename: principles.md
224+
Location: specs/
225+
"""
226+
227+
name: str = "Principles"
228+
version: str = "1.0"
229+
description: str = "Principles Document"
230+
231+
# Match principles.md specifically
232+
file_pattern: str = r"^principles\.md$"
233+
location_pattern: str = r"^specs/[^/]+\.md$"
234+
235+
# No identifier required for principles
236+
identifier: IdentifierSpec | None = None
237+
238+
sections: list[SectionSpec] = [
239+
SectionSpec(heading="Overview", heading_level=2, required=False),
240+
]
241+
242+
references: list[Reference] = []
243+
244+
245+
class SpecificationModule(SpecModule):
246+
"""
247+
Specification Document (SPEC).
248+
249+
Specifications document detailed requirements for a system or feature.
250+
They typically contain multiple requirements organized into sections.
251+
252+
Example filename: SPEC-001.md (or named like markdown-link-validator.md)
253+
Location: specs/
254+
"""
255+
256+
name: str = "Specification"
257+
version: str = "1.0"
258+
description: str = "Specification Document"
259+
260+
# Match files starting with SPEC- or specific spec names
261+
file_pattern: str = r"^(SPEC-\d{3}|markdown-[\w-]+|spec-[\w-]+)\.md$"
262+
# Match files directly in specs/ root, excluding subdirs
263+
location_pattern: str = r"^specs/[^/]+\.md$"
264+
265+
identifier: IdentifierSpec = IdentifierSpec(
266+
pattern=r"SPEC-\d{3}",
267+
location="metadata", # ID is in metadata section
268+
scope="global",
269+
)
270+
271+
sections: list[SectionSpec] = [
272+
SectionSpec(heading="Overview", heading_level=2, required=False),
273+
SectionSpec(heading="Requirements", heading_level=2, required=False),
274+
]
275+
276+
references: list[Reference] = [
277+
Reference(
278+
name="addresses",
279+
source_type="Specification",
280+
target_type="Job",
281+
cardinality=Cardinality(min=0, max=None),
282+
link_format="id_reference",
283+
),
284+
]
285+
286+
216287
# ============================================================================
217288
# Registry of Layer-Specific Types
218289
# ============================================================================
@@ -221,4 +292,6 @@ class ArchitectureDecisionModule(SpecModule):
221292
"Job": JobModule(),
222293
"Requirement": RequirementModule(),
223294
"ADR": ArchitectureDecisionModule(),
295+
"Specification": SpecificationModule(),
296+
"Principles": PrinciplesModule(),
224297
}

spec_check/dsl/reference_extractor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ def _infer_relationship(
263263
return "implements"
264264
if "dependencies" in section_lower or "prerequisites" in section_lower:
265265
return "depends_on"
266+
if "addresses" in section_lower:
267+
return "addresses"
266268

267269
# Check module definition for context
268270
if module_def and section:

spec_check/dsl/reference_resolver.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,10 @@ def _resolve_module_reference(
143143
warning = None
144144
if module_def and reference.relationship:
145145
ref_def = self._find_reference_definition(module_def, reference.relationship)
146-
if ref_def and ref_def.target_module:
147-
if target_module.module_type != ref_def.target_module:
146+
if ref_def and ref_def.target_type:
147+
if target_module.module_type != ref_def.target_type:
148148
warning = (
149-
f"Type mismatch: expected {ref_def.target_module}, "
149+
f"Type mismatch: expected {ref_def.target_type}, "
150150
f"found {target_module.module_type}"
151151
)
152152

spec_check/dsl/validator.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
- Pass 7: Reference Resolution (reference_resolver.py)
1212
"""
1313

14+
import re
1415
from dataclasses import dataclass, field
1516
from pathlib import Path
1617

17-
from spec_check.ast_parser import parse_markdown_file
18+
from spec_check.ast_parser import MarkdownDocument, parse_markdown_file
1819
from spec_check.dsl.id_registry import IDRegistry
1920
from spec_check.dsl.models import SpecModule
2021
from spec_check.dsl.reference_extractor import (
@@ -145,6 +146,9 @@ class DocumentContext:
145146
content: str
146147
"""Markdown content."""
147148

149+
parsed_doc: MarkdownDocument | None = None
150+
"""Parsed markdown document with metadata."""
151+
148152
module_def: SpecModule | None = None
149153
"""Matched module definition."""
150154

@@ -283,6 +287,7 @@ def _process_document(self, file_path: Path) -> None:
283287
self.documents[file_path] = DocumentContext(
284288
file_path=file_path,
285289
content=content,
290+
parsed_doc=doc,
286291
section_tree=section_tree,
287292
)
288293

@@ -366,7 +371,15 @@ def _extract_module_id(self, doc_ctx: DocumentContext, module_def: SpecModule) -
366371
if root.subsections and root.subsections[0].section_id:
367372
return root.subsections[0].section_id
368373

369-
# TODO: Support frontmatter and other locations
374+
elif location == "metadata":
375+
# Extract from document metadata (frontmatter or bold key-value pairs)
376+
if doc_ctx.parsed_doc and doc_ctx.parsed_doc.metadata:
377+
# Look for ID field in metadata
378+
for key, value in doc_ctx.parsed_doc.metadata.items():
379+
if key.upper() == "ID" and re.match(module_def.identifier.pattern, value):
380+
return value
381+
382+
# TODO: Support heading and other locations
370383

371384
return None
372385

specs/jobs/markdown-link-validator.md

Lines changed: 0 additions & 127 deletions
This file was deleted.

0 commit comments

Comments
 (0)