Skip to content

feat(catalog): feature 010 Phase E — assembled generator (T069–T086) - #90

Merged
mbeacom merged 1 commit into
mainfrom
mbeacom-phase-e-assembled-generator
Aug 5, 2026
Merged

feat(catalog): feature 010 Phase E — assembled generator (T069–T086)#90
mbeacom merged 1 commit into
mainfrom
mbeacom-phase-e-assembled-generator

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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/main on evidence/frozen-expectations/ and evidence/accept-corpus-freeze/ is empty. The frozen trees were not touched, and check:freeze-hashes is green.
  • No comparison harness was written. That stays Phase F, deliberately — authoring it here would collapse ADR-0020 clause 5's two distinct steps into one.

Checks

bun test 1721 pass / 0 fail (was 1504) · typecheck clean · check:deps ok · check:freeze-hashes ok · adr lint 20 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-directory restored.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 provenance decision

The two-value domain (upstream-authored | maintainer-overlay) was decided on 2026-08-05. Both values describe an annotation that exists — but annotation-absent is the dominant real-corpus case: zero of 156 community-plugins descriptor 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 on main as of this PR — verified: the latest data-model.md commit on origin/main is 77d2b08 and does not carry the new wording): provenance is a declaration about the source, carried onto every derived entity, and vacuous for annotation-absent entities — 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 to upstream-authored would silently convert an omission into a third-party adoption claim. This also gives other-invalid-input a 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-ref is unreachable from descriptor input by construction. canonicalize populates allRefs as [canonicalId], so two descriptors that canonicalize alike collide as duplicate-canonical-id first. 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 CLARIFICATION anticipates "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.ts discharges 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

  • collisionReason reported a within-entity ref repeat as duplicate-canonical-id, naming two entities where there was one.
  • The stage trace recorded per-entity repeats of ownership/glob.

One shared-file change

EXCLUDED_FROM_SCAN gained a sixth entry (test/envelope-only.test.ts) and its pinned assertion was updated to match. FR-038's guard must name CatalogSnapshot in order to forbid it — the same shape as the two consumer guards already listed. This is the documented relief valve in package-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.

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>
Copilot AI balanced review requested due to automatic review settings August 5, 2026 22:47
@mbeacom
mbeacom merged commit f7384ff into main Aug 5, 2026
10 checks passed
@mbeacom
mbeacom deleted the mbeacom-phase-e-assembled-generator branch August 5, 2026 22:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 as allMaintainerOverlay: {} 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 or Object.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
```
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