feat: prune per-unit design artifacts by unit kind (2.2.18) - #511
Conversation
leandrodamascena
left a comment
There was a problem hiding this comment.
I checked the three things that could break the prune logic and they hold. Both ends — resolveProduces and unitCovered — call filterProducesByKind(node.produces_kinds, node.produces, unitKind) on the same inputs, with the kind from one source (readBoltDagUnitKinds -> bolt_dag) at both points, so there's no directive/coverage mismatch or deadlock. An unmapped artifact stays for every kind; only an explicitly-mapped one prunes — the conservative direction. And the all-vacuous approve is right: in producesArtifactsExist the exemption fires only when every unit prunes to empty, while a single untagged unit gets ?? null -> full matrix -> still blocks on the disk check. No wrong-approve, no deadlock.
The one real issue is the #509 composition. resolveProduces now filters node.produces by kind but drops the optional_produces concat that #509 added, so an optional artifact never reaches the directive. And the produces_kinds: {frontend-components: [ui]} mapping points at frontend-components, which #509 moved into optional_produces — the engine only filters over node.produces, so that entry is dead and a ui unit never gets frontend-components. Same for shared-infrastructure. The fix is to filter the combined list: filterProducesByKind(node.produces_kinds, [...(node.produces ?? []), ...(node.optional_produces ?? [])], unitKind), and map the kinds against whichever list holds the artifact.
Two smaller things:
- Kind is enum-checked only at units-generation parse;
readBoltDagUnitKindsjust doestypeof === "string". A unit hand-tagged with a valid-but-wrong kind over-prunes silently — worth a line in the prose (matches thereadBoltDagBatchestrust posture). - Add a test asserting a
uiunit's directive carriesfrontend-componentsand a non-ui unit's omits it — the composition the PR describes but nothing exercises.
| unitKind: string | null = null, | ||
| ): string[] { | ||
| return (node.produces ?? []).map((name) => | ||
| return filterProducesByKind(node.produces_kinds, node.produces ?? [], unitKind).map((name) => |
There was a problem hiding this comment.
This filters node.produces by kind but drops the [...produces, ...optional_produces] concat that #509 (optional_produces, already on v2) added here — so an optional artifact never reaches the directive. And frontend-components, which #509 moved into optional_produces, is what produces_kinds: {frontend-components: [ui]} maps: since the filter only reads node.produces, that mapping is dead and a ui unit never gets it (same for shared-infrastructure). Filter the combined list instead: filterProducesByKind(node.produces_kinds, [...(node.produces ?? []), ...(node.optional_produces ?? [])], unitKind), and keep the kind map pointed at whichever list holds the artifact. unitCovered needs the matching treatment (kind-filter the required list, keep optional exempt from coverage per #509).
56fad96 to
0031220
Compare
|
Thanks for catching the #509 seam - that was exactly the collision. The branch predated
Version stays 2.2.12 (2.2.11 is claimed by #508). Smoke+unit green (141 files, 0 failed) and the integration/SDK tier green on the rebased tip. |
0031220 to
004b20d
Compare
… compiled-graph kind trust posture
004b20d to
f5fb2af
Compare
Closes #502.
What
Units get a kind, and the four per-unit construction design stages prune their artifact matrix to what applies:
service | spec | ui | packaging | library) in the machine-readableunits:edge block ofunit-of-work-dependency.md. The kind enum is validated at parse time: an out-of-enum value makes the edge blockmalformed, which the required-sections sensor fails at the units-generation gate, so a typo cannot reach the engine.produces_kinds:frontmatter map (artifact -> kinds). Stages without the key, and units without a kind, keep the full matrix - existing projects compile byte-identically.filterProducesByKind) applied at both ends:resolveProduces(the run-stage directive never tells the conductor to write a pruned artifact) andunitCovered(coverage never blocks on one). The approve-path guard handles the all-vacuous case: a stage where every tagged unit's artifacts are pruned gates and approves cleanly, while a single untagged unit (owing the full matrix) blocks the exemption.Version 2.2.12, CHANGELOG entry, README badge. Tests: t207 (schema: kind parses through the edge block and compile; sensor passes both shapes; kindless fixtures byte-identical) and t208 (engine: pruned directive paths, coverage completion without pruned files, untagged fallback, all-vacuous approve + negative control, code-generation untouched).
Why
The four design stages are
for_each: unit-of-workwith fixed ~5-file produces lists applied identically to every unit: roughly 20 mandated design documents per unit before any code, blind to what a unit is. A packaging unit owesbusiness-logic-model.md; a spec unit owesscalability-requirements.md. For non-service units most of these can only restate the unit's one real concern, and the doc matrix is the largest structural multiplier on wall-clock and token spend in a standard-depth run (4 stages x N units x ~5 files). Kind-aware produces turns the per-unit selectivity that currently requires filler documents into a compiled, auditable decision, with unknown-kind = full matrix as the conservative default.Relationship to siblings
resolveProduces(filter each list by kind, then concat) and theunitCovereddoc comment (take both). Theproduces_kindsunion rule already admits the artifacts fix: exempt optional produces from per-unit coverage so conditional artifacts can be skipped (2.2.9) #509 moves, sofrontend-components: [ui]stays valid post-merge.Verification
package.ts --check, designer-export--check, typecheck, and the coverage-registry ratchet all clean.produces_kinds:block parses to{}and is dropped on emit (round-trip not byte-stable for that pathological hand-authored input); no shipped stage has one and the packager is deterministic, so no drift is possible.runtime-graph.json(bypassing compile) over-prunes rather than falling back; this matches the pre-existing posture ofreadBoltDagBatchestoward the compiled graph.Gate: the combined pre-merge live gate ran GREEN on 2026-07-06 against a local integration branch composing all five sibling PRs (2.2.9-2.2.13) in version order - smoke+unit, Claude SDK integration, deterministic e2e, Claude TUI, Kiro ACP, Kiro TUI, and Codex exec slices, live vars set explicitly per slice. The one codex red (compose-front) reproduced identically on clean v2 and is a pre-existing sandbox fragility, not from this series. Merge in version order; whoever merges later re-bumps per the CHANGELOG conflict-trap policy.