feat(intent): export DDD model as contract-shaped intent.json - #8
feat(intent): export DDD model as contract-shaped intent.json#8tango238 wants to merge 2 commits into
Conversation
…tent.json Export docs/domain/*.md to a machine-readable intent.json so downstream tools (crawl-kit's verification layer) can consume intent as data, not prose — the "emit at the source" boundary. The diff axis that matters is events + state transitions (domain behaviour is too fine-grained to diff mechanically), so the payload leads with those plus a thin concept list for canonical-id seeding. Shape is a superset of the legacy glossary fixture, so existing consumers keep working. State transitions are decided INTERACTIVELY during the aggregates phase (new Step 3.5 + a `**状態遷移**:` template block), not guessed from prose, so the export is deterministic. The exporter reads that explicit block. - scripts/emit_intent.py: parser + CLI (stdlib only, JP/EN labels) - scripts/test_emit_intent.py: 7 stdlib tests (events, transitions, glossary) - references/phase-aggregates.md: Step 3.5 + 状態/状態遷移 template - SKILL.md: Intent Export section
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e5651d479
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if not b: | ||
| continue | ||
| indent, rest = len(b.group("indent")), b.group("rest") | ||
| if indent == 0: | ||
| collecting_transitions = labelled_bullet(rest, *L_TRANSITIONS) == "" |
There was a problem hiding this comment.
Parse the documented transition block shape
When aggregates.md follows the new template in references/phase-aggregates.md:116-117 (**状態遷移**: followed by unindented - from → to ...), this parser never emits any transition: the non-bullet section label is skipped before this block, and then the actual transition bullet reaches the indent == 0 path here and disables collection instead of parsing it. As a result, docs produced by the skill's own template export an empty stateTransitions array, removing the advertised verification oracle.
Useful? React with 👍 / 👎.
| cells = [c.strip() for c in line.strip().strip("|").split("|")] | ||
| if len(cells) < 2: | ||
| continue | ||
| term, english = cells[0], cells[1] |
There was a problem hiding this comment.
Restrict glossary parsing to term/English tables
For any glossary that includes the template's later コンテキスト横断の注意点 or コードとの不整合 tables, this line blindly treats column 2 as the English name and adds bogus concepts such as a definition string or コード上の名前. That pollutes intent.json's concepts and can seed incorrect canonical IDs; the parser needs to recognize only the actual 用語 | 英語 | 定義 / Term | English | ... tables before appending rows.
Useful? React with 👍 / 👎.
… prose
The exporter only began collecting a "**状態遷移**:" block when the label's
value was exactly empty, so an annotated label
("状態遷移: (コード由来…)") silently dropped its sub-bullets. Start
collecting whenever the label is present. Adds a regression test (an
events-less, code-derived ConceptState block with trailing prose).
Why
distill-ddd owns intent ("what we meant to build"). Downstream tools — notably crawl-kit's verification layer — need that intent as data, not prose. This adds the "emit at the source" boundary: the skill stays independent and only a contract-shaped JSON crosses over. Markdown stays the human artifact;
intent.jsonis its machine-readable twin.What
scripts/emit_intent.pyreadsdocs/domain/*.mdand emits:{ "concepts": [ { "name", "kind", "context", "aliases", "attributes" } ], // id seeding "events": [ { "name", "aggregate", "context", "trigger", "properties", "consumer" } ], "stateTransitions": [ { "aggregate", "context", "from", "to", "trigger", "event" } ] }The diff axis that actually matters for intent↔structure is events + state transitions (domain behaviour is too fine-grained to diff mechanically), so the payload leads with those.
conceptsis a superset of the legacy glossary fixture ({ "concepts": [...] }), so existing consumers keep working.domain-events.md**状態遷移**:block inaggregates.mdaggregates.mdheadings, aliases enriched fromglossary.mdState transitions are decided interactively, not guessed
Rather than have the exporter heuristically parse arrows out of prose, transitions are confirmed with the user during the
aggregatesphase:references/phase-aggregates.md: new Step 3.5 (elicit states + transitions) and a**状態**:/**状態遷移**:template block with the canonical line shape- {from} → {to} : \{trigger}` → `{event}``.**状態遷移**: なし.Tests
scripts/test_emit_intent.py— 7 stdlib-only tests (no pytest), matching the publish toolchain convention. Covers events, explicit transitions (both→and発行forms), the "なし" case, glossary alias enrichment, end-to-end, and missing-file tolerance.No regression in
test_diagrams.py. Verified end-to-end on a real model (crawl-kit/docs/domain): 7 concepts, 9 events parsed cleanly.