Skip to content

feat: prune per-unit design artifacts by unit kind (2.2.18) - #511

Merged
apackeer merged 2 commits into
v2from
feature/issue-502-unit-kind-artifact-pruning
Jul 9, 2026
Merged

feat: prune per-unit design artifacts by unit kind (2.2.18)#511
apackeer merged 2 commits into
v2from
feature/issue-502-unit-kind-artifact-pruning

Conversation

@apackeer

@apackeer apackeer commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Closes #502.

What

Units get a kind, and the four per-unit construction design stages prune their artifact matrix to what applies:

  • units-generation tags each unit with a kind (service | spec | ui | packaging | library) in the machine-readable units: edge block of unit-of-work-dependency.md. The kind enum is validated at parse time: an out-of-enum value makes the edge block malformed, which the required-sections sensor fails at the units-generation gate, so a typo cannot reach the engine.
  • The four construction design stages declare per-kind applicability in a new presence-gated produces_kinds: frontmatter map (artifact -> kinds). Stages without the key, and units without a kind, keep the full matrix - existing projects compile byte-identically.
  • The engine prunes in ONE exported helper (filterProducesByKind) applied at both ends: resolveProduces (the run-stage directive never tells the conductor to write a pruned artifact) and unitCovered (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.
  • code-generation and the swarm path are untouched.

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-work with 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 owes business-logic-model.md; a spec unit owes scalability-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

Verification

  • Adversarial verification pass: both prune points confirmed on the single helper; unknown-kind unreachable through the normal pipeline (parser throws -> sensor fails loud); untagged-unit block on the vacuous exemption proven via the coverage path; tests non-vacuous against origin/v2 (the schema does not exist there).
  • Deterministic tiers green (full run 229/230 files; the one red, t72, is the documented live reverse-engineering latency flake: red twice under cross-worktree contention, then green solo on this branch AND on the clean base; the RE stage file is byte-identical to v2). package.ts --check, designer-export --check, typecheck, and the coverage-registry ratchet all clean.
  • Known cosmetic edge: an entry-less 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.
  • Compiled-graph trust posture: a bad kind hand-edited into runtime-graph.json (bypassing compile) over-prunes rather than falling back; this matches the pre-existing posture of readBoltDagBatches toward 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.

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; readBoltDagUnitKinds just does typeof === "string". A unit hand-tagged with a valid-but-wrong kind over-prunes silently — worth a line in the prose (matches the readBoltDagBatches trust posture).
  • Add a test asserting a ui unit's directive carries frontend-components and a non-ui unit's omits it — the composition the PR describes but nothing exercises.

Comment thread core/tools/aidlc-orchestrate.ts Outdated
unitKind: string | null = null,
): string[] {
return (node.produces ?? []).map((name) =>
return filterProducesByKind(node.produces_kinds, node.produces ?? [], unitKind).map((name) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@apackeer
apackeer force-pushed the feature/issue-502-unit-kind-artifact-pruning branch from 56fad96 to 0031220 Compare July 9, 2026 01:34
@apackeer

apackeer commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for catching the #509 seam - that was exactly the collision. The branch predated optional_produces landing on v2 (both PRs were authored in parallel off b67798c), so the old tip's resolveProduces never saw the concat. Addressed as you suggested, plus the two smaller asks:

  • Rebased onto v2 @ 2.2.10 and resolved the seam your way: resolveProduces now kind-filters the COMBINED [...produces, ...optional_produces] list, so an optional artifact still reaches the directive for the kinds it applies to, and the frontend-components: [ui] / shared-infrastructure map entries are live (the schema validator already accepted keys from either list - it unions produces + optional_produces, so the kind map stays pointed at the artifact wherever it lives). unitCovered keeps the [Feature]: relevance-gate the per-unit construction design artifacts by unit kind (every unit currently gets the identical ~20-doc matrix) #502 arity and stays keyed off required produces[] only: kind-filtered, optional exempt, per fix: exempt optional produces from per-unit coverage so conditional artifacts can be skipped (2.2.9) #509.
  • Trust posture documented: readBoltDagUnitKinds prose (and the stage-definition reference) now states that kind: is enum-checked at units-generation parse only; the compiled graph is shape-checked (typeof === "string"), matching readBoltDagBatches - a hand-tagged valid-but-wrong kind in the compiled graph prunes silently.
  • Composition test added (t208 cases 8/8b): a ui unit's functional-design directive carries frontend-components.md and a service unit's omits it; and 8b pins that the optional artifact stays coverage-exempt even for the kind it applies to (a ui unit that wrote only business-logic-model advances without frontend-components on disk).

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.

@apackeer
apackeer requested a review from leandrodamascena July 9, 2026 01:35
@apackeer
apackeer force-pushed the feature/issue-502-unit-kind-artifact-pruning branch from 0031220 to 004b20d Compare July 9, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants