Skip to content

Commit 1fd7b97

Browse files
Pedro Anisio Silvaclaude
andcommitted
feat(recomposer): natural-description build plan generator (second delivery)
Adds a top-level `recomposer/` package that consumes a Decomposer YAML document (never the raw bundle) and emits an ordered, dependency-aware, evidence-grounded reconstruction plan as Markdown + YAML: - model.py: BuildStep/BuildPlan with the canonical 12-phase Part III sequence (skeleton -> environment -> domain -> contracts -> core -> adapters -> delivery -> persistence -> config -> tests -> validation -> docs). - plan.py: scheduler. Construction units are module parts; mutually dependent modules (SCCs of the decomposition's own dependency edges, not quality-gate parsing) merge into joint steps with a granularity-qualified cycle assumption. Phase relaxation guarantees a dependency is never scheduled after its dependent; the no-forward-references invariant is asserted at generation time. - render.py / serialize.py: natural-language Markdown guide (clipped lists) + full machine-readable YAML, both carrying the evidence-basis disclaimer banner; every step lists goal, rationale, requires, creates, contracts, dependencies, validation, evidence, expected result, confidence, and assumptions-to-resolve. Evidence-free phases are reported as skipped with reasons. - cli.py: `python -m recomposer <decomposition.yaml> [--plan] [--yaml]`. Output is byte-deterministic for a given decomposition. 19 tests under tests/recomposer/ (synthetic scheduler contracts + end-to-end against a real bundle). Also makes the report's "Semantic domains" assertion in tests/decomposer conditional on concept evidence, since bundles built without the concept plugin legitimately have no domain parts. AI-generated: Claude Fable 5 via Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 93b9d74 commit 1fd7b97

11 files changed

Lines changed: 1424 additions & 1 deletion

File tree

recomposer/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Repository Recomposer
2+
3+
[⬆ back to project root](../README.md)
4+
5+
## Disclaimer
6+
7+
This work is subject to the methodological caveats and commitments described in [@DISCLAIMER.md](../DISCLAIMER.md).
8+
> No statement or premise not backed by a real logical definition or verifiable reference should be taken for granted.
9+
10+
## What it does
11+
12+
Second delivery of the Decomposer/Recomposer system. Consumes a **Decomposer
13+
YAML document** — never the raw bundle or repository — and generates a
14+
**Natural Description Build Plan**: an ordered, dependency-aware,
15+
evidence-grounded sequence of natural-language construction steps that could
16+
recreate the system from scratch, executable by a human engineer or an AI
17+
coding agent.
18+
19+
```bash
20+
# 1) produce a decomposition (first delivery)
21+
python -m decomposer _tmp/<bundle> --yaml decomposition.yaml
22+
23+
# 2) recompose it into a build plan
24+
python -m recomposer decomposition.yaml --plan buildplan.md --yaml buildplan.yaml
25+
```
26+
27+
With no output flags, `python -m recomposer <yaml>` prints a phase-by-phase
28+
summary.
29+
30+
## How the plan is scheduled
31+
32+
1. **Units.** Module/package parts are the unit of construction. Modules that
33+
are mutually dependent in the decomposition's own dependency edges (SCCs of
34+
`dependencies.outgoing`) merge into one **joint step** — the evidence says no
35+
linear order exists among them. Cycle detection does *not* parse
36+
quality-gate findings, whose names/formats are reporting policy; the
37+
scheduler depends only on the data contract.
38+
2. **Nominal phases.** Each unit gets a canonical Part III phase from its
39+
classification (domain→3, ports/shared-kernel→4, core/supporting→5,
40+
adapter/infrastructure→6, test→10); fixed steps (skeleton, environment,
41+
schemas, ops, validation, docs) take phases 1, 2, 3, 9, 11, 12.
42+
3. **Phase relaxation.** Dependency evidence overrides canon: a dependency is
43+
never scheduled after its dependent (single pass in descending build-order
44+
layer, correct because dependencies sit at strictly lower layers in the
45+
SCC-condensed DAG).
46+
4. **Invariant.** Every `requires` reference points to an earlier step; this is
47+
asserted at generation time (`ValueError` on violation), not assumed.
48+
49+
Each step carries: goal, rationale, required previous steps, files/components
50+
to create, contracts to define, dependencies introduced, tests/validation
51+
required, evidence (part ids + coupling metrics), expected result, a confidence
52+
label (`certain`/`strong`/`probable`/`weak`/`unknown`), and explicit
53+
**assumptions to resolve** (Part IV). Phases with no evidence are reported as
54+
skipped with a reason — never silently.
55+
56+
Output is **byte-deterministic** for a given decomposition document.
57+
58+
## Epistemics (PALS's Law)
59+
60+
File inventories, dependency edges, and build order are mechanically derived
61+
facts carried over from the Decomposer. Step goals, rationale, and
62+
responsibilities are interpretive and confidence-tagged. LLM-authored text
63+
appears only as evidence explicitly marked `LLM (unverified)`. Both emitted
64+
documents (Markdown and YAML) carry an evidence-basis disclaimer banner.
65+
66+
## Layout
67+
68+
| file | responsibility |
69+
|---|---|
70+
| `model.py` | `BuildStep` / `BuildPlan` dataclasses, canonical 12-phase table |
71+
| `plan.py` | scheduler: units, SCC merging, phase relaxation, ordering, requires |
72+
| `render.py` | natural-language Markdown build plan |
73+
| `serialize.py` | machine-readable YAML build plan (full file lists) |
74+
| `cli.py` / `__main__.py` | `python -m recomposer` |
75+
76+
Tests: `tests/recomposer/` (synthetic scheduler tests + end-to-end contract
77+
tests against a real bundle decomposition).
78+
79+
---
80+
*AI-generated (Claude Fable 5 via Claude Code); reviewed under the project's
81+
verification rules.*

recomposer/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Repository Recomposer — second delivery.
2+
3+
Consumes a Decomposer YAML document (never the raw bundle or repository) and
4+
generates a **Natural Description Build Plan**: an ordered, dependency-aware,
5+
evidence-grounded sequence of natural-language construction steps that could
6+
recreate the system from scratch, executable by a human engineer or an AI
7+
coding agent.
8+
9+
Public API:
10+
recompose(doc: dict) -> BuildPlan
11+
to_markdown(plan) -> str
12+
to_yaml(plan) -> str
13+
"""
14+
from __future__ import annotations
15+
16+
from .model import PHASES, BuildPlan, BuildStep
17+
from .plan import recompose
18+
from .render import to_markdown
19+
from .serialize import to_document, to_yaml
20+
21+
__all__ = [
22+
"recompose",
23+
"to_markdown",
24+
"to_yaml",
25+
"to_document",
26+
"BuildPlan",
27+
"BuildStep",
28+
"PHASES",
29+
]
30+
31+
__version__ = "0.1.0"

recomposer/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""``python -m recomposer`` entry point."""
2+
from __future__ import annotations
3+
4+
from .cli import main
5+
6+
if __name__ == "__main__":
7+
raise SystemExit(main())

recomposer/cli.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Command-line interface for the Repository Recomposer.
2+
3+
python -m recomposer <decomposition.yaml> [--plan OUT.md] [--yaml OUT.yaml] [--stdout]
4+
5+
Consumes a Decomposer YAML document — never the raw bundle — and emits the
6+
Natural Description Build Plan as Markdown and/or YAML. With no output flags it
7+
prints a short summary.
8+
"""
9+
from __future__ import annotations
10+
11+
import argparse
12+
import sys
13+
from pathlib import Path
14+
15+
import yaml
16+
17+
from .model import PHASE_TITLE
18+
from .plan import recompose
19+
from .render import to_markdown
20+
from .serialize import to_yaml
21+
22+
23+
def main(argv: list[str] | None = None) -> int:
24+
parser = argparse.ArgumentParser(
25+
prog="python -m recomposer",
26+
description="Generate an ordered natural-language reconstruction plan "
27+
"from a Decomposer YAML document.",
28+
)
29+
parser.add_argument("decomposition", type=Path,
30+
help="Path to a decomposition YAML produced by "
31+
"`python -m decomposer ... --yaml`.")
32+
parser.add_argument("--plan", type=Path, default=None,
33+
help="Write the Markdown build plan to this path.")
34+
parser.add_argument("--yaml", type=Path, default=None,
35+
help="Write the YAML build plan to this path.")
36+
parser.add_argument("--stdout", action="store_true",
37+
help="Print the Markdown plan to stdout.")
38+
args = parser.parse_args(argv)
39+
40+
if not args.decomposition.exists():
41+
parser.error(f"decomposition file not found: {args.decomposition}")
42+
doc = yaml.safe_load(args.decomposition.read_text())
43+
if not isinstance(doc, dict) or "parts" not in doc:
44+
parser.error(f"not a decomposition document (no `parts`): {args.decomposition}")
45+
46+
plan = recompose(doc)
47+
48+
wrote = False
49+
if args.plan:
50+
args.plan.write_text(to_markdown(plan))
51+
print(f"wrote Markdown build plan -> {args.plan}", file=sys.stderr)
52+
wrote = True
53+
if args.yaml:
54+
args.yaml.write_text(to_yaml(plan))
55+
print(f"wrote YAML build plan -> {args.yaml}", file=sys.stderr)
56+
wrote = True
57+
if args.stdout:
58+
print(to_markdown(plan))
59+
wrote = True
60+
61+
if not wrote:
62+
phases = sorted({s.phase for s in plan.steps})
63+
print(f"repository: {plan.repository.get('name')}")
64+
print(f"steps: {len(plan.steps)} across {len(phases)} phases")
65+
for n in phases:
66+
k = sum(1 for s in plan.steps if s.phase == n)
67+
print(f" phase {n:>2} ({PHASE_TITLE[n]}): {k} step(s)")
68+
print(f"skipped phases: {len(plan.skipped_phases)}")
69+
print(f"open assumptions: {len(plan.open_assumptions)}")
70+
print("(pass --plan/--yaml/--stdout to emit full output)")
71+
return 0
72+
73+
74+
if __name__ == "__main__":
75+
raise SystemExit(main())

recomposer/model.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Recomposition data model — the Natural Description Build Plan.
2+
3+
A :class:`BuildPlan` is an ordered sequence of :class:`BuildStep` records that,
4+
followed in order, reconstruct the system described by a Decomposer output.
5+
Every step is evidence-grounded (it cites the decomposition part ids and files
6+
it derives from) and confidence-tagged; steps resting on unresolved assumptions
7+
carry them explicitly (Part IV: "reconstruction steps that depend on unresolved
8+
assumptions" must be reported, never silently embedded).
9+
10+
The Recomposer consumes ONLY the decomposition document — never the raw bundle
11+
or repository — so this model mirrors what that document can prove.
12+
"""
13+
from __future__ import annotations
14+
15+
from dataclasses import dataclass, field
16+
from typing import Any
17+
18+
# Canonical construction phases (Part III). The scheduler may pull a step to an
19+
# earlier phase than its nominal one when dependency evidence forces it (e.g. a
20+
# cycle group spanning core and infrastructure must be built together); it never
21+
# pushes a step later than its dependents.
22+
PHASES: list[tuple[int, str, str]] = [
23+
(1, "skeleton", "Establish project skeleton"),
24+
(2, "environment", "Configure package/build/runtime environment"),
25+
(3, "domain_data", "Define core domain/data model"),
26+
(4, "contracts", "Define internal contracts/interfaces"),
27+
(5, "core_logic", "Implement core logic"),
28+
(6, "adapters_infrastructure", "Implement adapters/infrastructure"),
29+
(7, "delivery_surfaces", "Implement APIs/CLIs/jobs/events"),
30+
(8, "persistence", "Implement persistence and migrations"),
31+
(9, "configuration_deployment", "Implement configuration and deployment"),
32+
(10, "tests_fixtures", "Implement tests and fixtures"),
33+
(11, "validation", "Validate full-system behavior"),
34+
(12, "documentation", "Document usage and extension points"),
35+
]
36+
PHASE_TITLE: dict[int, str] = {n: title for n, _, title in PHASES}
37+
PHASE_KEY: dict[int, str] = {n: key for n, key, _ in PHASES}
38+
39+
40+
@dataclass
41+
class BuildStep:
42+
number: int
43+
phase: int # 1..12, index into PHASES
44+
goal: str
45+
rationale: str # construction intent: why now, why this shape
46+
requires: list[int] = field(default_factory=list) # earlier step numbers
47+
creates: list[str] = field(default_factory=list) # files/components to create
48+
contracts: list[str] = field(default_factory=list) # interfaces/symbols to define
49+
dependencies_introduced: list[str] = field(default_factory=list)
50+
tests_required: list[str] = field(default_factory=list)
51+
evidence: list[str] = field(default_factory=list) # part ids + signals
52+
expected_result: str = ""
53+
confidence: str = "probable"
54+
assumptions: list[str] = field(default_factory=list) # unresolved assumptions
55+
parts: list[str] = field(default_factory=list) # decomposition part ids realized
56+
57+
def to_dict(self) -> dict[str, Any]:
58+
return {
59+
"step": self.number,
60+
"phase": self.phase,
61+
"phase_title": PHASE_TITLE.get(self.phase, "?"),
62+
"goal": self.goal,
63+
"rationale": self.rationale,
64+
"requires_steps": list(self.requires),
65+
"creates": list(self.creates),
66+
"contracts": list(self.contracts),
67+
"dependencies_introduced": list(self.dependencies_introduced),
68+
"tests_required": list(self.tests_required),
69+
"evidence": list(self.evidence),
70+
"expected_result": self.expected_result,
71+
"confidence": self.confidence,
72+
"assumptions": list(self.assumptions),
73+
"parts": list(self.parts),
74+
}
75+
76+
77+
@dataclass
78+
class BuildPlan:
79+
repository: dict[str, Any] # copied from the decomposition header
80+
architecture_intent: dict[str, Any] # style + hypotheses the rebuild should honor
81+
steps: list[BuildStep] = field(default_factory=list)
82+
skipped_phases: list[dict[str, str]] = field(default_factory=list)
83+
open_assumptions: list[str] = field(default_factory=list)
84+
provenance: dict[str, Any] = field(default_factory=dict)
85+
86+
def to_dict(self) -> dict[str, Any]:
87+
return {
88+
"repository": self.repository,
89+
"architecture_intent": self.architecture_intent,
90+
"steps": [s.to_dict() for s in self.steps],
91+
"skipped_phases": list(self.skipped_phases),
92+
"open_assumptions": list(self.open_assumptions),
93+
"provenance": self.provenance,
94+
}

0 commit comments

Comments
 (0)