feat(catalog): feature 010 Phase D — adapter pure validators (T038–T068) - #86
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
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. |
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.
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/ordocs/adr/— never to the oracle expectation set or the clause-5 accept-corpus expected paths. Nothing underevidence/frozen-expectations/orevidence/accept-corpus-freeze/was read or referenced. No generator pipeline was assembled, noSnapshotEnvelopeproduced, 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§6Locationworked example; SC-008 close-out.D1a — admissibility and identity (T047–T056). Descriptor reading keeping
duplicate-yaml-keyandinvalid-yaml-syntaxas two distinct outcomes; ADR-0015 four field validators transcribed from its table and separately attributed; the separator rule;inadmissible-descriptorclassification carrying path, failing field, and the rejecting validator; two-step canonicalization; SC-004 close-out.D1b — ownership and glob (T057–T068). Ownership derived from
adrkit.io/owned-pathsalone; ordered annotation decode with step 2 checking the raw YAML node beforeJSON.parse; the three ownership states withexplicit-emptydecided on the decoded value; the fifteen ordered glob rules, first-match-wins; compile-once-per-run;compareCodeUnitsordering 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:annotation-value-not-a-stringwrong-shapearray<string>parse-errorJSON.parsefailsStep 1 (presence) yields
annotation-absent, which is an ownership state, not a rejection. Step 5 (per-pattern) yieldsinvalid-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 testpass / 0 fail ·typecheckclean ·check:deps→core-has-no-adapter-deps: ok·adr lint20 records, 0 errors.Scope held: nothing under
packages/catalog-envelope/**(concurrent Phase C tree), nopackage.json, notsconfig.json, noscripts/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:
contracts/admissibility.md§2 contradicts ADR-0015 on the namespace character class. §2 givesmetadata.namespacethe class[A-Za-z0-9]plus-,_,.— identical tometadata.name. ADR-0015 and FR-016 bind it throughCommonValidatorFunctions.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') === falseis the discriminating assertion.data-model.md§4'sAdmissibilityFieldis wrong in both directions — omitsapiVersion, which ADR-0015 requires; addsspec.type, which no validator in the table covers.data-model.md§3'sDescriptorDocumentomitsapiVersion, so a record built to §3 could not be admissibility-checked at all. AddedrawApiVersionandraw, flagged in-source.input-manifest.md§1 andatomic-fail-closed.md§4 disagree on the closed-schema trigger class. §4 was followed as later and more specific.input-manifest.md§4.1 names no trigger class for path-validation stage 1.invalid-manifest-shapeattributed as an inference from the silence, flagged in-source.input-manifest.md§2 anddata-model.md§1 disagree onrequiredCapabilitiesarity. §2's rejection rule implemented as written; no arity rejection invented that the contract does not authorize.isValidDnsSubdomain's per-label character class — only its bounds. Composed as dot-separated labels each satisfying theisValidDnsLabelpredicate stated in the same table. The only place transcription alone was insufficient; reproduces all four facts ADR-0015 records as executed against the pin.import.meta.resolveanywhere in the adapter; T063 requires the picomatch version be read at runtime from the resolved dependency. Satisfied both by readingnode_modules/picomatch/package.jsonfrom disk — a filesystem read, not a resolver invocation. Reads4.0.5, matching R3. Confirmation wanted that this is the intended reading rather than a route around the guard.source-scan.tsimportSpecifiersheuristic 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… frombefore a quote as an import. Labels were renamed to route around it rather than weaken the guard, but any later phase writing'…import'orfrom '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:
""fall through to rule 12 and reportempty-segment, notempty— first-match-wins failing visibly.source-unreadable, notsource-missing. Same trigger class, so the class alone would not have caught it.Note for Phase E
The fifteen-member
TriggerClassunion lives insrc/diagnostics.ts, not insrc/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.