feat(catalog): feature 010 Phase E — assembled generator (T069–T086) - #90
Merged
Conversation
Compose the Phase D validators into `src/pipeline.ts` in T069's fixed nine-stage
order, enforce whole-operation atomicity across the fifteen fatal trigger classes,
and emit the versioned envelope as the only output.
New source under packages/adapters/catalog-backstage/src/:
- pipeline.ts — the assembled generator; records the stages it entered so the
ordering is observable rather than asserted
- failure/{triggers,abort,classify}.ts — the fifteen-value enumeration re-exported
from diagnostics.ts (never redeclared), the abort record and exit-code mapping,
and a contract-sourced reason→class registry so the correctness check is not a
tautology
- identity/{uniqueness,overlap}.ts — global uniqueness over every ref with three
distinct collision classes and no first/last-wins; overlap positively
demonstrated as not-a-collision
- envelope/{completeness,provenance,shape,digest,write}.ts — wholeCatalog pinned
false, the closed two-value provenance domain, the nine/five field shapes, the
digest via @adrkit/core's canonicalStringify, and an atomic single-file write
Sixteen new test files; the suite goes from 1504 to 1721 passing.
Phase E is behind Barrier B, which cleared at T024. The frozen trees under
evidence/frozen-expectations/ and evidence/accept-corpus-freeze/ are byte-identical
to origin/main; check:freeze-hashes stays green. No comparison harness was written —
that is Phase F.
Fifteen deliberate mutations were each observed failing and then reverted, retained
under evidence/negative-cases/{triggers,whole-operation-atomicity,
envelope-invariants,determinism}/ per ADR-0016.
T078 and T086 are left unchecked, with reasons:
- T078 asks for all fifteen triggers through the full assembled pipeline. Fourteen
went through it. `duplicate-canonical-ref` is unreachable from descriptor input
because allRefs holds only the canonical id, so it was exercised at the
canonicalization stage's uniqueness kernel with a synthetic identity set.
- T086's second limb needs a pass over the frozen accept corpus. Those descriptor
files are not in this repository — the freeze records the corpus metadata, not
the descriptors — so no fixture here meets ADR-0020 clause 5's conditions.
ADR-0014 rung 1 only. No release is scheduled, implied, or prepared.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Implements Phase E’s assembled Backstage catalog generator, including fail-closed execution, deterministic envelope generation, and mutation evidence.
Changes:
- Composes the fixed nine-stage generation pipeline.
- Adds atomic envelope writing, provenance, uniqueness, digest, and determinism handling.
- Adds extensive tests and retained negative-case evidence.
Reviewed changes
Copilot reviewed 70 out of 70 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
specs/010-catalog-backstage/tasks.md |
Updates Phase E task status. |
packages/adapters/catalog-backstage/src/pipeline.ts |
Assembles the generation pipeline. |
packages/adapters/catalog-backstage/src/failure/abort.ts |
Defines atomic failure outcomes. |
packages/adapters/catalog-backstage/src/failure/classify.ts |
Validates trigger classifications. |
packages/adapters/catalog-backstage/src/failure/triggers.ts |
Defines the fatal-trigger surface. |
packages/adapters/catalog-backstage/src/identity/uniqueness.ts |
Enforces global reference uniqueness. |
packages/adapters/catalog-backstage/src/identity/overlap.ts |
Models overlapping ownership. |
packages/adapters/catalog-backstage/src/envelope/completeness.ts |
Fixes completeness semantics. |
packages/adapters/catalog-backstage/src/envelope/digest.ts |
Computes canonical envelope digests. |
packages/adapters/catalog-backstage/src/envelope/provenance.ts |
Handles provenance declarations. |
packages/adapters/catalog-backstage/src/envelope/shape.ts |
Defines the envelope shape. |
packages/adapters/catalog-backstage/src/envelope/write.ts |
Writes envelopes atomically. |
packages/adapters/catalog-backstage/test/pipeline-fixtures.ts |
Provides integration fixtures. |
packages/adapters/catalog-backstage/test/pipeline-stages.test.ts |
Tests pipeline ordering. |
packages/adapters/catalog-backstage/test/completeness-always-false.test.ts |
Tests completeness invariants. |
packages/adapters/catalog-backstage/test/uniqueness.test.ts |
Tests collision handling. |
packages/adapters/catalog-backstage/test/overlap.test.ts |
Tests union ownership semantics. |
packages/adapters/catalog-backstage/test/abort.test.ts |
Tests atomic abort behavior. |
packages/adapters/catalog-backstage/test/backstop-trigger.test.ts |
Tests the backstop trigger. |
packages/adapters/catalog-backstage/test/trigger-classification.test.ts |
Tests trigger mapping. |
packages/adapters/catalog-backstage/test/sc-002-mixed-batch.test.ts |
Tests mixed-batch atomicity. |
packages/adapters/catalog-backstage/test/sc-003-all-triggers.test.ts |
Exercises fatal triggers. |
packages/adapters/catalog-backstage/test/envelope-only.test.ts |
Tests single-artifact output. |
packages/adapters/catalog-backstage/test/envelope-shape.test.ts |
Tests serialized shape. |
packages/adapters/catalog-backstage/test/envelope-digest.test.ts |
Tests digest generation. |
packages/adapters/catalog-backstage/test/envelope-provenance.test.ts |
Tests provenance behavior. |
packages/adapters/catalog-backstage/test/byte-identical.test.ts |
Tests byte stability. |
packages/adapters/catalog-backstage/test/sc-001-determinism.test.ts |
Tests accept/reject determinism. |
packages/adapters/catalog-backstage/test/sc-009.test.ts |
Records partial SC-009 coverage. |
packages/adapters/catalog-backstage/test/sc-013.test.ts |
Tests envelope close-out. |
packages/adapters/catalog-backstage/test/source-scan.ts |
Extends scanner exclusions. |
packages/adapters/catalog-backstage/test/no-dynamic-loader.test.ts |
Pins the updated exclusions. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/README.md |
Documents atomicity mutations. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/case-1-skip-the-bad-entity-and-keep-going.patch |
Records skip mutation. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/case-1-skip-the-bad-entity-and-keep-going.observed.txt |
Captures skip-mutation failure. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/case-2-envelope-written-on-the-failure-branch.patch |
Records failure-write mutation. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/case-2-envelope-written-on-the-failure-branch.observed.txt |
Captures failure-write detection. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/case-3-uniqueness-resolved-by-last-wins.patch |
Records last-wins mutation. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/case-3-uniqueness-resolved-by-last-wins.observed.txt |
Captures last-wins detection. |
specs/010-catalog-backstage/evidence/negative-cases/whole-operation-atomicity/restored.observed.txt |
Records restored atomicity tests. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/README.md |
Documents trigger mutations. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-1-collapse-duplicate-yaml-key-into-invalid-yaml-syntax.patch |
Records YAML-class collapse. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-1-collapse-duplicate-yaml-key-into-invalid-yaml-syntax.observed.txt |
Captures YAML-class detection. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-2-registry-maps-unrecognized-field-to-a-version-problem.patch |
Records manifest misclassification. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-2-registry-maps-unrecognized-field-to-a-version-problem.observed.txt |
Captures registry detection. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-3-trigger-count-transcribed-as-fourteen.patch |
Records trigger-count drift. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-3-trigger-count-transcribed-as-fourteen.observed.txt |
Captures count-drift detection. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-4-backstop-made-unreachable.patch |
Records unreachable-backstop mutation. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-4-backstop-made-unreachable.observed.txt |
Captures backstop detection. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-5-backstop-reason-remapped-to-a-named-class.patch |
Records backstop remapping. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-5-backstop-reason-remapped-to-a-named-class.observed.txt |
Captures remapping detection. |
specs/010-catalog-backstage/evidence/negative-cases/triggers/restored.observed.txt |
Records restored trigger tests. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/README.md |
Documents envelope mutations. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-1-whole-catalog-completeness-claimed.patch |
Records completeness mutation. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-1-whole-catalog-completeness-claimed.observed.txt |
Captures completeness detection. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-2-entity-record-spreads-instead-of-projecting.patch |
Records shape-leak mutation. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-2-entity-record-spreads-instead-of-projecting.observed.txt |
Captures shape-leak detection. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-3-digest-over-a-non-canonical-serialization.patch |
Records digest mutation. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-3-digest-over-a-non-canonical-serialization.observed.txt |
Captures digest detection. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-4-provenance-defaulted-to-upstream-authored.patch |
Records provenance defaulting. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-4-provenance-defaulted-to-upstream-authored.observed.txt |
Captures provenance detection. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-5-a-side-file-written-alongside-the-envelope.patch |
Records side-file mutation. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-5-a-side-file-written-alongside-the-envelope.observed.txt |
Captures side-file detection. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-6-overlap-resolved-by-an-exclusive-winner.patch |
Records exclusive-owner mutation. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/case-6-overlap-resolved-by-an-exclusive-winner.observed.txt |
Captures overlap detection. |
specs/010-catalog-backstage/evidence/negative-cases/envelope-invariants/restored.observed.txt |
Records restored envelope tests. |
specs/010-catalog-backstage/evidence/negative-cases/determinism/README.md |
Documents determinism mutation. |
specs/010-catalog-backstage/evidence/negative-cases/determinism/case-1-entity-order-made-non-deterministic.patch |
Records randomized ordering. |
specs/010-catalog-backstage/evidence/negative-cases/determinism/case-1-entity-order-made-non-deterministic.observed.txt |
Captures determinism detection. |
specs/010-catalog-backstage/evidence/negative-cases/determinism/restored.observed.txt |
Records restored determinism tests. |
Suppressed comments (1)
packages/adapters/catalog-backstage/src/envelope/provenance.ts:252
- This helper has the same
__proto__key failure asallMaintainerOverlay:{}cannot safely serve as a dictionary for unrestricted valid source paths, so an explicitly upstream-authored__proto__source is lost. Use a null-prototype record orObject.fromEntries.
const bySourcePath: Record<string, AnnotationProvenance> = {};
for (const path of sourcePaths) bySourcePath[path] = 'upstream-authored';
| * Run the generator. Returns the envelope, or exactly one | ||
| * {@link AtomicFailureRecord} — never both, and never a partial envelope. | ||
| */ | ||
| export async function runGeneration(request: GenerationRequest): Promise<GenerationResult> { |
Comment on lines
+478
to
+480
| const outcome = await runGeneration(request); | ||
| if (!outcome.ok) return { ok: false, failure: outcome.failure }; | ||
| return { ok: true, envelope: outcome.envelope, write: await writeEnvelope(outcome.envelope, destination) }; |
|
|
||
| // Same directory as the destination: a cross-device rename is not atomic, and a | ||
| // temporary file under the system temp directory is very often on another device. | ||
| const temporary = join(directory, `.${envelope.digest}.envelope.tmp`); |
Comment on lines
+237
to
+238
| const bySourcePath: Record<string, AnnotationProvenance> = {}; | ||
| for (const path of sourcePaths) bySourcePath[path] = 'maintainer-overlay'; |
Comment on lines
+119
to
+126
| ``` | ||
| (fail) T079 — exactly one file is written > a successful run leaves one file, and it is the envelope | ||
| (fail) T079 — exactly one file is written > a second run into the same directory still leaves one file | ||
| (fail) T079 — exactly one file is written > a pre-existing unrelated file is left alone rather than cleaned up | ||
| (fail) T079 — diagnostics are returned, never written > the stage trace is a returned value and appears in no file | ||
| (fail) T079 — writeEnvelope writes one file and creates its directory > a nested destination directory is created | ||
| (fail) T085 / SC-013 — exactly one envelope is produced > the destination directory holds exactly one file | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The assembled generator — pipeline, whole-operation atomicity, and envelope emission. T069–T086, the first phase operating behind Barrier B.
Barrier discipline held
git diff origin/mainonevidence/frozen-expectations/andevidence/accept-corpus-freeze/is empty. The frozen trees were not touched, andcheck:freeze-hashesis green.Checks
bun test1721 pass / 0 fail (was 1504) ·typecheckclean ·check:depsok ·check:freeze-hashesok ·adr lint20 records, 0 errors.ADR-0016 evidence
Fifteen deliberate mutations, each observed failing and reverted, retained under
evidence/negative-cases/{triggers,whole-operation-atomicity,envelope-invariants,determinism}/with.patch,.observed.txt, and per-directoryrestored.observed.txt.The fifteen-trigger table is generated from live run output (
ADRKIT_PRINT_TRIGGERS=1), not transcribed — so it cannot drift from the code.One case is labelled honestly weak: the provenance-default mutation fails only one test, because the earlier declaration check already refuses the request. A reader can see which observations are load-bearing and which are nearly redundant.
A gap found in the
provenancedecisionThe two-value domain (
upstream-authored|maintainer-overlay) was decided on 2026-08-05. Both values describe an annotation that exists — butannotation-absentis the dominant real-corpus case: zero of 156community-pluginsdescriptor files carry the annotation. Neither value was literally true of such an entity.No third value was invented. The generator records the source's declared provenance, and
isAdoptionClaim(ownershipState, provenance)was added because the pair is unambiguous even where the field alone is not.data-model.md§10 has since been reconciled by the maintainer to match (not yet onmainas of this PR — verified: the latestdata-model.mdcommit onorigin/mainis77d2b08and does not carry the new wording):provenanceis a declaration about the source, carried onto every derived entity, and vacuous forannotation-absententities — no adoption claim in either direction.Nothing carried the declaration.
input-manifest.md§1's schema is closed at five fields, so the manifest cannot. It is now a required, exhaustive part of the generation request, with no default in either direction — defaulting toupstream-authoredwould silently convert an omission into a third-party adoption claim. This also givesother-invalid-inputa genuine, non-contrived reachability path, which T075 needs; contrived reachability for a backstop trigger is worth little.Two tasks left honestly unchecked
T078 —
duplicate-canonical-refis unreachable from descriptor input by construction.canonicalizepopulatesallRefsas[canonicalId], so two descriptors that canonicalize alike collide asduplicate-canonical-idfirst. Fourteen of fifteen triggers went through the full assembled pipeline; this one was exercised at the canonicalization stage's uniqueness kernel with a synthetic identity set.T078's own
NEEDS CLARIFICATIONanticipates "reachable only via a synthetic fixture" — a kernel call is one step further than that, so the task was under-claimed rather than checked. A test asserts exactly one of the fifteen was reached this way, so the claim breaks if that ever changes. This is arguably a design property worth recording rather than a coverage gap; the maintainer call is open.T086 limb 2 — the accept corpus is not materialized in this repository. The freeze records corpus metadata, not descriptor files. Every fixture this phase could author is maintainer-authored, so none meets ADR-0020 clause 5's conditions, and reconstructing the descriptors from the freeze would be both non-conforming and circular.
sc-009.test.tsdischarges limbs 1 and 3 and carries an executable record of the gap that fails the day someone vendors the corpus. Note that T088 cannot run without it either — this is a Phase F prerequisite surfaced early, not a Phase E omission.Two of its own bugs, caught by its own tests
collisionReasonreported a within-entity ref repeat asduplicate-canonical-id, naming two entities where there was one.ownership/glob.One shared-file change
EXCLUDED_FROM_SCANgained a sixth entry (test/envelope-only.test.ts) and its pinned assertion was updated to match. FR-038's guard must nameCatalogSnapshotin order to forbid it — the same shape as the two consumer guards already listed. This is the documented relief valve inpackage-boundary.md§4, used instead of renaming around the scanner as two earlier sessions did.Standing
ADR-0014 rung 1 only. No rung-2 or rung-3 claim, no release scheduled, implied, or prepared. ADR-0012 gate 3 recorded as observed; gate 4 unmet and not yet testable.