Skip to content

feat(catalog): feature 010 Phase D — adapter pure validators (T038–T068) - #86

Merged
mbeacom merged 1 commit into
mainfrom
mbeacom-phase-d-adapter-validators
Aug 5, 2026
Merged

feat(catalog): feature 010 Phase D — adapter pure validators (T038–T068)#86
mbeacom merged 1 commit into
mainfrom
mbeacom-phase-d-adapter-validators

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Fills in the pure-validator implementation of packages/adapters/catalog-backstage/, created by Phase A. Three disjoint module slices.

Barrier-free, and verified as such. Every expected value traces to a contract frozen in specs/ or docs/adr/ — never to the oracle expectation set or the clause-5 accept-corpus expected paths. Nothing under evidence/frozen-expectations/ or evidence/accept-corpus-freeze/ was read or referenced. No generator pipeline was assembled, no SnapshotEnvelope produced, no input-manifest instance over a real corpus, no comparison harness.

What landed

D2 — input boundary (T038–T046). Closed manifest schema with single-repository binding; the three version/capability rejections; repository identity and revision read through separate git tooling and compared by exact string equality; per-source digest verification before any entity is processed; two-stage path validation (lexical, then confined realpath); the closed input boundary with input-manifest.md §6 Location worked example; SC-008 close-out.

D1a — admissibility and identity (T047–T056). Descriptor reading keeping duplicate-yaml-key and invalid-yaml-syntax as two distinct outcomes; ADR-0015 four field validators transcribed from its table and separately attributed; the separator rule; inadmissible-descriptor classification carrying path, failing field, and the rejecting validator; two-step canonicalization; SC-004 close-out.

Ordering is structural, not conventional. AdmittedDescriptor carries an unexported runtime brand, canonicalize accepts only that type, and collectAdmitted is all-or-nothing — so an inadmissible descriptor cannot reach a uniqueness comparison, in either direction. The ADR-0015 admissibility-before-canonicalization rule is enforced by the type system rather than by discipline.

D1b — ownership and glob (T057–T068). Ownership derived from adrkit.io/owned-paths alone; ordered annotation decode with step 2 checking the raw YAML node before JSON.parse; the three ownership states with explicit-empty decided on the decoded value; the fifteen ordered glob rules, first-match-wins; compile-once-per-run; compareCodeUnits ordering with deduplication; SC-005 close-out.

Evidence

Ten negative-case directories under evidence/negative-cases/. Twenty-three checks each observed failing with their exact emitted strings recorded, then restored and observed passing (ADR-0016).

Two tasks left honestly unchecked — a spec defect, not an implementation gap

T058 and T062 are - [ ]. Both require "five ordered annotation decode steps, each with its own distinct rejection reason" and five recorded reason strings. The frozen contract defines three.

specs/009-catalog-binding-viability/contracts/owned-paths-annotation.md §1 enumerates five ordered steps, but its own outcome table yields exactly three distinct rejection reasons:

Reason Trigger
annotation-value-not-a-string step 2 — raw node is not a string scalar
wrong-shape step 4 — decoded value is not array<string>
parse-error step 3 — JSON.parse fails

Step 1 (presence) yields annotation-absent, which is an ownership state, not a rejection. Step 5 (per-pattern) yields invalid-pattern, which belongs to the glob dialect trigger class, not annotation decode.

The task conflated step count with reason count. Manufacturing two additional reasons to turn the boxes green would have invented a vocabulary no contract froze. Both tasks are left unchecked with the reason recorded, for central reconciliation.

(Related and separately tracked: rule 15 of the glob dialect, invalid-glob-compile-failure, is a defensive backstop its own contract says is "expected to never occur in practice" — so rule 15 not firing is conformant and is not a coverage gap.)

Checks

bun test pass / 0 fail · typecheck clean · check:depscore-has-no-adapter-deps: ok · adr lint 20 records, 0 errors.

Scope held: nothing under packages/catalog-envelope/** (concurrent Phase C tree), no package.json, no tsconfig.json, no scripts/check-deps.ts, no spec/plan/contract text — checkboxes only.

Standing: ADR-0014 rung 1 only. No rung-2 or rung-3 claim, no release scheduled or prepared, and no generator output produced.


Full defect list — ten findings, reported not fixed

The section above covers finding 7. The complete list, all verified against source and none fixed by this PR:

  1. contracts/admissibility.md §2 contradicts ADR-0015 on the namespace character class. §2 gives metadata.namespace the class [A-Za-z0-9] plus -, _, . — identical to metadata.name. ADR-0015 and FR-016 bind it through CommonValidatorFunctions.isValidDnsLabel, /^[a-z0-9]+(?:\-+[a-z0-9]+)*$/, which admits no uppercase, no underscore, no dot. ADR-0015 was followed, per FR-016's "exactly the four field validators in ADR-0015's table". validateNamespace('Default') === false is the discriminating assertion.
  2. data-model.md §4's AdmissibilityField is wrong in both directions — omits apiVersion, which ADR-0015 requires; adds spec.type, which no validator in the table covers.
  3. data-model.md §3's DescriptorDocument omits apiVersion, so a record built to §3 could not be admissibility-checked at all. Added rawApiVersion and raw, flagged in-source.
  4. input-manifest.md §1 and atomic-fail-closed.md §4 disagree on the closed-schema trigger class. §4 was followed as later and more specific.
  5. input-manifest.md §4.1 names no trigger class for path-validation stage 1. invalid-manifest-shape attributed as an inference from the silence, flagged in-source.
  6. input-manifest.md §2 and data-model.md §1 disagree on requiredCapabilities arity. §2's rejection rule implemented as written; no arity rejection invented that the contract does not authorize.
  7. ADR-0015 does not state isValidDnsSubdomain's per-label character class — only its bounds. Composed as dot-separated labels each satisfying the isValidDnsLabel predicate stated in the same table. The only place transcription alone was insufficient; reproduces all four facts ADR-0015 records as executed against the pin.
  8. Phase A's loader guard collides with T063/FR-029. The guard forbids import.meta.resolve anywhere in the adapter; T063 requires the picomatch version be read at runtime from the resolved dependency. Satisfied both by reading node_modules/picomatch/package.json from disk — a filesystem read, not a resolver invocation. Reads 4.0.5, matching R3. Confirmation wanted that this is the intended reading rather than a route around the guard.
  9. Phase A's source-scan.ts importSpecifiers heuristic false-positives and will keep doing so. It matched the string 'bulk-import' — ADR-0015's own plugin name — as a side-effecting import, and prose ending in … from before a quote as an import. Labels were renamed to route around it rather than weaken the guard, but any later phase writing '…import' or from ' in a string will hit it.

Two wrong-reason failures worth noting

The ADR-0016 runs caught failures that were wrong in their reason, not merely absent — which a trigger-class assertion alone would have missed:

  • Disabling glob rule 1 makes "" fall through to rule 12 and report empty-segment, not empty — first-match-wins failing visibly.
  • Disabling the source-existence check makes a missing manifest-listed file report source-unreadable, not source-missing. Same trigger class, so the class alone would not have caught it.

Note for Phase E

The fifteen-member TriggerClass union lives in src/diagnostics.ts, not in src/failure/triggers.ts (Phase E's file, deliberately not created here). Phase E should import it rather than redeclare it — a second declaration of a closed enumeration is exactly the drift that file exists to prevent.

Fills in the pure-validator implementation of
`packages/adapters/catalog-backstage/`, created by Phase A. Three disjoint
module slices, all barrier-free: every expected value traces to a contract
frozen in `specs/` or `docs/adr/`, never to the oracle expectation set or the
clause-5 accept-corpus expected paths.

D2 — input boundary (T038–T046)
  Closed manifest schema with single-repository binding; the three
  version/capability rejections; repository identity and revision read through
  separate git tooling and compared by exact string equality; per-source digest
  verification before any entity is processed; two-stage path validation
  (lexical, then confined realpath); the closed input boundary with
  `input-manifest.md` §6's `Location` worked example; SC-008 close-out.

D1a — admissibility and identity (T047–T056)
  Descriptor reading keeping `duplicate-yaml-key` and `invalid-yaml-syntax` as
  two distinct outcomes; ADR-0015's four field validators transcribed from its
  table and separately attributed; the separator rule; `inadmissible-descriptor`
  classification carrying path, field and rejecting validator; two-step
  canonicalization; SC-004 close-out.

  Ordering is structural, not conventional: `AdmittedDescriptor` carries an
  unexported runtime brand, `canonicalize` accepts only that type, and
  `collectAdmitted` is all-or-nothing — so an inadmissible descriptor cannot
  reach a uniqueness comparison, in either direction.

D1b — ownership and glob (T057–T068)
  Ownership derived from `adrkit.io/owned-paths` alone; the ordered annotation
  decode steps with step 2 checking the raw YAML node before `JSON.parse`; the
  three ownership states with `explicit-empty` decided on the decoded value; the
  fifteen ordered glob rules, first-match-wins; compile-once-per-run;
  `compareCodeUnits` ordering with deduplication; SC-005 and SC-006 close-outs.

Evidence (ADR-0016): ten negative-case directories under
`specs/010-catalog-backstage/evidence/negative-cases/`. Twenty-three checks were
each observed failing with their exact emitted strings recorded, then restored
and observed passing.

Counting facts this change depends on: this feature's fatal trigger enumeration
has **fifteen** members (fourteen is spike 009's and is wrong here); the glob
dialect has **fifteen** ordered rules of which **fourteen** require exercise,
and rule 15 not firing is conformant and is not reported as a coverage gap.

ADR-0014 rung 1 only. No pipeline is assembled, no envelope is produced, no
`InputManifest` instance over a real corpus is built, and no comparison harness
exists. No artifact asserts what Backstage as a running system does; the warrant
throughout is what a pure validator predicate returns at the pinned commit
`1121a4facd9e321179d0402c3f355e4a649e84d9`.

T058 and T062 are left unchecked: both require "five" distinct annotation
rejection reasons, and the frozen contract defines exactly three.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 5, 2026 12:57
@mbeacom
mbeacom merged commit d8a58d5 into main Aug 5, 2026
10 checks passed
@mbeacom
mbeacom deleted the mbeacom-phase-d-adapter-validators branch August 5, 2026 13:02

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 D’s pure validators for the Backstage catalog adapter, without assembling the Phase E generation pipeline.

Changes:

  • Adds manifest, repository, digest, path, descriptor, and admissibility validation.
  • Adds ownership decoding, restricted-glob validation, canonicalization, and deterministic ordering.
  • Adds contract tests and mutation-based negative-case evidence.

Reviewed changes

Copilot reviewed 113 out of 113 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/adapters/catalog-backstage/src/diagnostics.ts Defines validator diagnostics.
packages/adapters/catalog-backstage/src/manifest/schema.ts Validates manifest shape.
packages/adapters/catalog-backstage/src/manifest/version.ts Checks versions and capabilities.
packages/adapters/catalog-backstage/src/manifest/digests.ts Verifies source digests.
packages/adapters/catalog-backstage/src/manifest/paths.ts Validates confined paths.
packages/adapters/catalog-backstage/src/manifest/boundary.ts Models the input boundary.
packages/adapters/catalog-backstage/src/repository/identity.ts Verifies repository identity.
packages/adapters/catalog-backstage/src/descriptor/read.ts Reads YAML descriptors.
packages/adapters/catalog-backstage/src/admissibility/separator.ts Implements separator handling.
packages/adapters/catalog-backstage/src/admissibility/validators.ts Implements field validators.
packages/adapters/catalog-backstage/src/admissibility/classify.ts Classifies inadmissibility.
packages/adapters/catalog-backstage/src/admissibility/index.ts Enforces admission ordering.
packages/adapters/catalog-backstage/src/identity/canonicalize.ts Canonicalizes admitted identities.
packages/adapters/catalog-backstage/src/ownership/annotation.ts Decodes ownership annotations.
packages/adapters/catalog-backstage/src/ownership/states.ts Defines ownership states.
packages/adapters/catalog-backstage/src/ownership/derive.ts Derives declared ownership.
packages/adapters/catalog-backstage/src/glob/dialect.ts Implements glob compilation.
packages/adapters/catalog-backstage/src/glob/validate.ts Applies ordered glob rules.
packages/adapters/catalog-backstage/src/glob/order.ts Orders and deduplicates paths.
packages/adapters/catalog-backstage/test/descriptor-read.test.ts Tests descriptor parsing.
packages/adapters/catalog-backstage/test/descriptor-fixtures.ts Supplies descriptor fixtures.
packages/adapters/catalog-backstage/test/manifest-single-repo.test.ts Tests closed manifest shape.
packages/adapters/catalog-backstage/test/manifest-version.test.ts Tests manifest versions.
packages/adapters/catalog-backstage/test/manifest-digests.test.ts Tests digest verification.
packages/adapters/catalog-backstage/test/manifest-paths.test.ts Tests path confinement.
packages/adapters/catalog-backstage/test/input-boundary.test.ts Tests read boundaries.
packages/adapters/catalog-backstage/test/repository-identity.test.ts Tests git identity reads.
packages/adapters/catalog-backstage/test/repository-exact-match.test.ts Tests exact comparison.
packages/adapters/catalog-backstage/test/admissibility-validators.test.ts Tests field predicates.
packages/adapters/catalog-backstage/test/admissibility-separator.test.ts Tests separator rules.
packages/adapters/catalog-backstage/test/admissibility-classify.test.ts Tests classification.
packages/adapters/catalog-backstage/test/admissibility-record.test.ts Tests diagnostic attribution.
packages/adapters/catalog-backstage/test/admissibility-ordering.test.ts Tests ordering constraints.
packages/adapters/catalog-backstage/test/admissibility-excluded-from-uniqueness.test.ts Tests uniqueness exclusion.
packages/adapters/catalog-backstage/test/inadmissible-and-unique.test.ts Tests independent admissibility.
packages/adapters/catalog-backstage/test/annotation-decode.test.ts Tests annotation decoding.
packages/adapters/catalog-backstage/test/annotation-step2-raw-node.test.ts Tests coercion prevention.
packages/adapters/catalog-backstage/test/ownership-states.test.ts Tests ownership states.
packages/adapters/catalog-backstage/test/glob-rules.test.ts Tests ordered glob rules.
packages/adapters/catalog-backstage/test/glob-mixed-batch.test.ts Tests mixed classifications.
packages/adapters/catalog-backstage/test/glob-compile-once.test.ts Tests compiler reuse.
packages/adapters/catalog-backstage/test/glob-order.test.ts Tests deterministic ordering.
packages/adapters/catalog-backstage/test/sc-004.test.ts Closes SC-004 coverage.
packages/adapters/catalog-backstage/test/sc-005.test.ts Closes SC-005 coverage.
packages/adapters/catalog-backstage/test/sc-006.test.ts Exercises annotation steps.
packages/adapters/catalog-backstage/test/sc-008.test.ts Closes SC-008 coverage.
specs/010-catalog-backstage/tasks.md Updates Phase D checkboxes.
specs/010-catalog-backstage/evidence/negative-cases/yaml-read/* Records YAML mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/repository-mismatch/* Records identity mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/path-validation/* Records path mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/manifest-version/* Records version mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/incomplete-required-source/* Records digest mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/inadmissible-and-unique/* Records admissibility evidence.
specs/010-catalog-backstage/evidence/negative-cases/glob-rules/* Records glob mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/annotation-sequence-coercion/* Records coercion evidence.
specs/010-catalog-backstage/evidence/negative-cases/annotation-decode/* Records decode mutation evidence.
specs/010-catalog-backstage/evidence/negative-cases/admissibility-validators/* Records validator evidence.

Comment on lines +263 to +264
const rawCapabilities = value['requiredCapabilities'];
if (!Array.isArray(rawCapabilities)) {
);
}

const manifest = (await Bun.file(manifestPath).json()) as { version?: unknown };
Comment on lines +25 to +27
expect(orderDerivedPaths(['a/**', 'B/**'])).toEqual(['B/**', 'a/**']);
expect('B'.localeCompare('a')).toBeGreaterThan(0);
expect(compareCodeUnits('B', 'a')).toBeLessThan(0);

import { type GlobCompiler, createGlobCompiler } from './dialect.ts';

/** `data-model.md` §7.1. Fifteen outcomes: fourteen rejections plus `accepted`. */

## Restored

[`restored.observed.txt`](./restored.observed.txt) — all 15 tests pass, 0 fail.

## Restored

[`restored.observed.txt`](./restored.observed.txt) — all 16 tests pass, 0 fail.

## Restored

[`restored.observed.txt`](./restored.observed.txt) — all 12 tests pass, 0 fail.

## Restored

[`restored.observed.txt`](./restored.observed.txt) — all 30 tests pass, 0 fail.

## Restored

[`restored.observed.txt`](./restored.observed.txt) — all 18 tests pass, 0 fail.

## Restored

[`restored.observed.txt`](./restored.observed.txt) — all 30 tests pass, 0 fail.
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