Cycle 156, phase-2 of the DDD/hex rescope. First phase-2 migration attempt (soc-8fjo: metrics_health.go → LoopReaderPort) surfaced a bigger finding than the rescope predicted: the function we planned to migrate has zero production callers. quality.LoadCycleHistory in cli/internal/quality/metrics_health.go and its one-line shim cli/cmd/ao/metrics_health.go::loadCycleHistory are defined, tested directly, and called by nothing else. Two tests in cli/cmd/ao/metrics_health_test.go probe the dead path. The right phase-2 move was delete, not migrate. Removed: - cli/internal/quality/metrics_health.go: LoadCycleHistory (23 lines) + now-unused bufio/encoding-json imports - cli/cmd/ao/metrics_health.go: loadCycleHistory shim (1 line) - cli/cmd/ao/metrics_health_test.go: TestMetricsHealth_LoadCycleHistory + TestMetricsHealth_LoadCycleHistoryMissingFile (~22 lines) Net: ~46 lines of dead code removed. Full test suite green. Drive-by fix while running the regression bundle: cobra_commands_test TestCobraExpectedCmdsMatchRegistration was failing because cycles 144-154 added 6 new top-level commands (ci, citation, claim, harness, loop, operator) without updating the expectedCmds list. That drift was pre-existing, surfaced by running the full bundle. Added the 6 to both expectedCmds lists in the test. Also surfaced (deferred, not done): cli/internal/lifecycle/defrag.go ::SweepOscillatingGoals keys on a 'Target' field that ports.CycleEntry does NOT project. Worse, zero entries in cycle-history.jsonl have a 'target' field — the production sweep is a permanent no-op. Migration to LoopReaderPort requires a schema decision (extend CycleEntry vs re-target defrag at a different artifact). Filed as deferred under soc-1q1x. Closing soc-8fjo (no longer applicable: target deleted). Updated docs/rescope/2026-05-13-ddd-hex-architecture-rescope.md with an addendum capturing both findings.
10 KiB
Rescope — DDD / Hex Architecture Arc
Date: 2026-05-13 Trigger: operator instruction during /evolve cycle 154 — "rescope all the work from the update principles and ddd/hex architecture work we just pushed" Scope: cycles ~80-154 (the BC ports + production adapters + CLI-wiring work)
What we built
| Layer | Count | Status |
|---|---|---|
| BC ports (interfaces + DTOs) | 14 | Complete — BC1 Corpus, BC2 Validation, BC3 Loop, BC4 Factory, BC5 Runtime |
| In-memory test doubles + tests | 14 | Complete |
| Production adapters | 14 | Complete |
| CLI surfaces exposing adapters | 10 (71%) | Partial — 4 adapters not yet CLI-exposed |
Compile-time var _ Port = (*production…)(nil) assertions |
14 | Complete |
Adapters: CorpusReader, CorpusWriter, FindingCompiler, Citation, GateRunner, ClaimEvidenceBinder, CIStatus, ClaimEvidence, LoopReader, LoopWriter, Operator, EventBus, FactoryAdmission, Harness.
CLI surfaces: ao corpus inject/capture, ao citation verify, ao ci latest/recent, ao gate run, ao loop history/append, ao operator record/list, ao harness status, ao claim bind/list.
What is missing — the load-bearing observation
Grep cli/internal/ports imports outside cli/cmd/ao/: zero.
Every consumer of the port surface lives in cli/cmd/ao/ — i.e., the CLI commands we just wired in cycles 144-154. The actual business logic in:
cli/internal/lifecycle/defrag.go(readscycle-history.jsonldirectly)cli/internal/quality/metrics_health.go(readscycle-history.jsonldirectly)cli/cmd/ao/context_assemble.go(readscycle-history.jsonldirectly)cli/internal/daemon/*(operator events, gate results, ledger bindings — all direct I/O)- skill scripts under
skills/**/*.sh(directjsonlparsing)
…still does direct filesystem I/O against the same artifacts the ports were extracted to encapsulate.
The ports exist; the architecture does not. The hex shape is half-built: outer ring (ports + adapters + CLI) is in, but the inner ring (business code that should route through ports) hasn't been migrated yet. We solved the easy half.
Why this happened (honest retrospective)
The CLI-wiring template (cycle-147) became a compounding metronome — 7 consecutive cycles, ~285 LOC each, ~10 min each, 8 tests each. That cadence is gratifying and produces commits, but each new adapter just exposes one more latent capability rather than activating an existing one. The cycle-122 "wire-up arc" learning predicted this exact failure mode: "production adapters are latent until cross-package consumers exist."
We then went from extracting ports → wrapping them with production adapters → wrapping the adapters with CLI commands. We never went the other direction: rewiring real consumers (defrag, metrics_health, context_assemble, daemon, skills) to route through the ports.
In, out, deferred
IN — finish-line work (the half we haven't done)
- Rewire
cli/internal/lifecycle/defrag.goto useLoopReaderPort. Single highest-value migration: defrag is core knowledge-compounding code and currently readscycle-history.jsonlraw. - Rewire
cli/internal/quality/metrics_health.goto useLoopReaderPort. Currently has its ownloadCycleHistoryhelper (the one that collided with the CLI'sloadCycleHistoryin cycle 144 — drift evidence). - Rewire
cli/cmd/ao/context_assemble.goto useLoopReaderPort+CorpusReaderPort. Knowledge injection should route through the typed Corpus surface, not direct file walk. - Decide: are the remaining 4 adapter CLI surfaces required? Only if their value is "a real consumer needs them"; otherwise they're decorative.
OUT — stop doing
- No more "wrap adapter N with CLI command" cycles. The template is proven, the marginal value is zero until real consumers exist. The 4 unexposed adapters (FactoryAdmission, ClaimEvidence, EventBus, FindingCompiler) stay unexposed until a consumer needs them, not because we want a 100% number.
- No more port extractions until the existing 14 are load-bearing. We don't need a 15th port until at least 5 of the existing ports have real (non-CLI) consumers.
- No more
bc-ports-inventory.mdupdates. It's already reconciled.
DEFERRED — keep tracked, don't work now
- HypothesisLedgerPort + ConvergenceCheckPort (
soc-y5vh.3) — only useful once /rpi or /evolve actually compose with them. Not yet. - PG3/PG4 auto-promotion logic (
soc-2klg.3,soc-2klg.4) — substantial work; only pay it down when the claim ledger has live consumers. - Codex skills parity for the new
aosubcommands —ao harness statussurfaced 77 OutOfSync codex skills (cycle 149); that's a separate epic, not part of this arc.
Closure criteria for the DDD/Hex arc
This rescope declares the arc PHASE-1 COMPLETE, PHASE-2 PENDING:
- ✅ Phase 1 (done): hex shape extracted — 14 ports, 14 production adapters, 10 CLI surfaces, all tested, all reconciled in
docs/contracts/bc-ports-inventory.md. - ⏳ Phase 2 (next): hex adoption — at least 3 non-CLI consumers (defrag, metrics_health, context_assemble) routed through ports. Until then, the ports are latent infrastructure, not architecture.
Phase 1 took ~70 cycles. Phase 2's first three migrations should each fit in 1-2 cycles if scoped tightly (grep the direct-IO call sites → swap to loopReaderPort.ReadHistory(...) → keep regression tests).
Open epic disposition
| Epic | Status | Action |
|---|---|---|
soc-2c1p BC1 Corpus ports |
Phase-1 complete | Close after Phase-2 first wave (Corpus consumers migrated) |
soc-wxh5 BC2 Validation ports |
Phase-1 complete | Close after Phase-2 first wave |
soc-y5vh BC3 Loop ports |
Phase-1 complete | Close after defrag.go + metrics_health.go + context_assemble.go migrate |
soc-2klg BC4 Factory ports |
Phase-1 complete | Phase-2 deferred (no daemon migration yet) |
soc-zd7c BC5 Runtime ports |
Phase-1 complete | Phase-2 deferred (harness has no internal consumers) |
Concrete next-cycle picks (when /evolve resumes the loop)
hex-phase2-loop-defrag— Rewirecli/internal/lifecycle/defrag.goto depend onLoopReaderPortinstead of readingcycle-history.jsonldirectly. First proof that the ports are load-bearing.hex-phase2-loop-metrics-health— Same forcli/internal/quality/metrics_health.go. Removes the duplicateloadCycleHistoryhelper that collided in cycle 144.hex-phase2-loop-context-assemble— Same forcli/cmd/ao/context_assemble.go. Closes the BC3 loop reader migration.
After those land, the architecture statement "BC3 is hexagonal" is finally true.
Lessons from the arc
- Hex extraction is a two-phase commitment. Extracting the port + writing the adapter is the warm-up. Migrating consumers is the actual architectural change. Stopping after phase 1 leaves you with the cost of indirection and none of the benefit.
- Compounding metronomes are seductive failure modes. The CLI-wiring template was beautifully repeatable. But repeatability is not the same as value-delivery. After 3-4 applications, the marginal return per cycle dropped to near-zero and we kept going on momentum.
- Operator override is a feature, not friction. This rescope only happened because the operator interrupted the cron loop. The /evolve work-selection ladder doesn't currently distinguish "exposing latent infrastructure" from "rewiring real consumers" — both look productive to the regression gate.
What "rescope" produced
This document. Plus the three concrete phase-2 picks above, ready to be filed as bd beads when the loop resumes.
Bottom line: stop adding adapters and CLI commands. Start rewiring real consumers (defrag, metrics_health, context_assemble) to use the ports we already built. Three migrations close the BC3 loop and prove the hex architecture is load-bearing rather than decorative.
Addendum (cycle 156) — first migration target was dead code
Update from the first phase-2 attempt. When opening cli/internal/quality/metrics_health.go::LoadCycleHistory to migrate it to LoopReaderPort, two findings surfaced:
-
Schema mismatch on defrag.go.
defrag.go::SweepOscillatingGoalsreads aTargetfield thatCycleEntrydoes NOT project. Worse, zero entries in the actualcycle-history.jsonlhave atargetfield — the production sweep is a permanent no-op against a per-goal schema that never materialized. Migrating defrag.go to LoopReaderPort would not be a clean swap; it'd require either (a) addingTargettoCycleEntryand the ledger writers, or (b) routing defrag at a different artifact (e.g. goals.jsonl). -
metrics_health.LoadCycleHistoryis dead code.loadCycleHistoryis defined incli/cmd/ao/metrics_health.go:375as a one-line shim aroundquality.LoadCycleHistory, but the shim has zero production callers. The only references are: the shim's own definition, the underlyingqualityfunction's definition, and two tests that probe the dead path directly. The cycle 144loadCycleHistorycollision was actually a collision against dead code — the cleanup was already overdue.
Action taken in cycle 156: deleted quality.LoadCycleHistory (~23 lines), the shim in cmd/ao/metrics_health.go (1 line), and the two dead tests in cmd/ao/metrics_health_test.go (~22 lines). Net: ~46 lines of dead code removed, full test suite still green.
Rescope thesis re-validated: the architecture exists, but the production code that should consume it never did. We kept building infrastructure for consumers that turned out to be either schema-mismatched (defrag) or dead code (metrics_health). The cleaner phase-2 path is delete dead code AND migrate context_assemble.go, not "migrate all three" as originally listed. Closing soc-8fjo (metrics_health migration) — no longer applicable, the target is gone.
Updated phase-2 picks (cycle 156 forward):
soc-8fjo— RESOLVED via deletion in cycle 156.soc-0pku— context_assemble.go → LoopReaderPort + CorpusReaderPort. Still the right migration.soc-1q1x— defrag.go → LoopReaderPort. Deferred, pending Target-field schema decision onCycleEntry. Filed as a discussion-then-migration follow-up, not an immediate cycle pick.