diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4a20507..f11c6a3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,8 @@ jobs: run: git diff --exit-code packages/ci/dist - name: Verify dependency boundaries (adapters + toolkit confinement) run: bun run check:deps + - name: Verify frozen oracle hashes match (feature 010 Barrier B, R5 mechanism 2) + run: bun run check:freeze-hashes - name: Lint ADR corpus run: bun run adr lint diff --git a/package.json b/package.json index 29fb6aae..9a04ae0f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "lint": "bun run --filter='*' lint", "schema:emit": "bun run --filter='@adrkit/core' schema:emit", "check:deps": "bun run scripts/check-deps.ts", + "check:freeze-hashes": "bun run scripts/check-freeze-hashes.ts", "audit:gate": "bun run scripts/audit-gate.ts", "adr": "bun packages/cli/src/index.ts", "release:pack": "bun scripts/release-pack.ts", diff --git a/scripts/audit-oracle-freeze.test.ts b/scripts/audit-oracle-freeze.test.ts new file mode 100644 index 00000000..c6a9decc --- /dev/null +++ b/scripts/audit-oracle-freeze.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from 'bun:test'; +import { join } from 'node:path'; +import { + auditFromEvidence, + auditOracleFreeze, + canonicalHash, + REASON_NO_ADEQUACY, + REASON_ORDER_NOT_COMPARE_CODE_UNITS, +} from './audit-oracle-freeze.ts'; + +const EVIDENCE = join(process.cwd(), 'specs/010-catalog-backstage/evidence'); +const NEG = join(EVIDENCE, 'negative-cases'); + +describe('T019 audit procedure — observed passing on the live freeze', () => { + test('the real audit passes against the live evidence tree', async () => { + const result = await auditFromEvidence(EVIDENCE); + expect(result).toEqual({ ok: true, findings: [] }); + }); +}); + +describe('T020 — observed failing: derivedPathPatterns in input order', () => { + test('the real audit FAILS with the ordering reason against the retained input-order variant', async () => { + const result = await auditFromEvidence(join(NEG, 'oracle-input-order')); + expect(result.ok).toBe(false); + // The variant recomputes its own hash, so ordering is the SOLE finding — proving the + // audit catches the spike-009 defect independent of integrity. + expect(result.findings.map((f) => f.check)).toEqual(['ordering']); + expect(result.findings[0]!.reason).toBe(REASON_ORDER_NOT_COMPARE_CODE_UNITS); + }); + + test('and PASSES again once the correct compareCodeUnits-ordered artifact is used (restore)', async () => { + const result = await auditFromEvidence(EVIDENCE); + expect(result).toEqual({ ok: true, findings: [] }); + }); +}); + +describe('T021 — observed failing: integrity confirmed but adequacy never reached', () => { + test('the real audit records FAIL against SC-010, not a silent accept', async () => { + const result = await auditFromEvidence(join(NEG, 'audit-integrity-only')); + expect(result.ok).toBe(false); + // Integrity is intact (hashes match), so the ONLY finding is the missing adequacy + // determination — the exact failure mode the adequacy requirement exists to prevent. + expect(result.findings.map((f) => f.check)).toEqual(['adequacy']); + expect(result.findings[0]!.reason).toBe(REASON_NO_ADEQUACY); + }); + + test('a bare integrity-only audit input (no adequacy field) is rejected in isolation too', () => { + // Minimal reproduction independent of files: correct hashes, no adequacy finding. + const frozen: Record = { derivedPathPatterns: [] }; + frozen.contentHash = canonicalHash(frozen); + const accept: Record = {}; + accept.contentHash = canonicalHash(accept); + const result = auditOracleFreeze({ + frozenExpectationSet: frozen, + acceptCorpusFreeze: accept, + // adequacy deliberately omitted + }); + expect(result.ok).toBe(false); + expect(result.findings.map((f) => f.check)).toEqual(['adequacy']); + expect(result.findings[0]!.reason).toBe(REASON_NO_ADEQUACY); + }); + + test('and PASSES again once an explicit adequacy finding is recorded (restore)', async () => { + const result = await auditFromEvidence(EVIDENCE); + expect(result).toEqual({ ok: true, findings: [] }); + }); +}); diff --git a/scripts/audit-oracle-freeze.ts b/scripts/audit-oracle-freeze.ts new file mode 100644 index 00000000..76e94e83 --- /dev/null +++ b/scripts/audit-oracle-freeze.ts @@ -0,0 +1,147 @@ +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; + +/** + * The T019 oracle-freeze audit procedure, encoded as an executable so that its + * FAIL behaviour can be observed (ADR-0016), not merely asserted. This is the same + * procedure the independent auditor ran by hand in T019; making it a program lets + * T020 and T021 attack it with deliberately bad input and record the exact reason + * strings it emits. + * + * It performs three checks: + * 1. content-hash integrity — recompute each artifact's canonical hash and compare + * to the recorded contentHash (evidence/README.md §3 canonical form); + * 2. derivedPathPatterns ordering — confirm compareCodeUnits order, not input order; + * 3. adequacy — require an explicit adequacy finding (ADR-0020 clause 5(a)); + * hash integrity alone is NOT sufficient and must be recorded as a FAIL + * against SC-010, never silently accepted. + */ + +/** compareCodeUnits: UTF-16 code-unit ordering. JS relational operators ARE this. */ +export function compareCodeUnits(a: string, b: string): -1 | 0 | 1 { + return a < b ? -1 : a > b ? 1 : 0; +} + +/** README §3 canonical form: object with contentHash removed, keys ascending by + * compareCodeUnits, array order preserved, no insignificant whitespace, JSON string + * escaping, UTF-8, no trailing newline. */ +export function canonicalize(value: unknown): string { + if (value === null || typeof value === 'number' || typeof value === 'boolean') { + return JSON.stringify(value); + } + if (typeof value === 'string') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`; + const obj = value as Record; + const keys = Object.keys(obj).sort(compareCodeUnits); + return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`; +} + +export function canonicalHash(artifact: Record): string { + const { contentHash: _omit, ...rest } = artifact; + return createHash('sha256').update(Buffer.from(canonicalize(rest), 'utf8')).digest('hex'); +} + +export interface AuditFinding { + check: 'content-hash' | 'ordering' | 'adequacy'; + reason: string; +} + +export interface AdequacyFinding { + /** Must be exactly 'adequate' or 'inadequate'; anything else (including absence) + * means the audit never reached an adequacy determination. */ + finding?: 'adequate' | 'inadequate'; + reasoning?: string; +} + +export interface AuditInput { + frozenExpectationSet: Record; + acceptCorpusFreeze: Record; + /** The adequacy finding the auditor recorded. Its ABSENCE is the T021 failure mode. */ + adequacy?: AdequacyFinding; +} + +export interface AuditResult { + ok: boolean; + findings: AuditFinding[]; +} + +// Exact reason strings — frozen so the observed-failing tests can match them verbatim. +export const REASON_HASH_DRIFT = 'content hash does not match recomputed canonical hash'; +export const REASON_ORDER_NOT_COMPARE_CODE_UNITS = + 'derivedPathPatterns is not in compareCodeUnits order (input order or any other order is inadmissible)'; +export const REASON_NO_ADEQUACY = + 'audit confirmed integrity but recorded no adequacy finding — SC-010 requires an explicit adequacy determination, an integrity confirmation alone does not satisfy clause 5(a)'; + +export function auditOracleFreeze(input: AuditInput): AuditResult { + const findings: AuditFinding[] = []; + + // 1. content-hash integrity for both artifacts. + for (const artifact of [input.frozenExpectationSet, input.acceptCorpusFreeze]) { + const recorded = artifact.contentHash; + const recomputed = canonicalHash(artifact); + if (recorded !== recomputed) { + findings.push({ check: 'content-hash', reason: REASON_HASH_DRIFT }); + } + } + + // 2. derivedPathPatterns ordering — must be exactly compareCodeUnits-sorted. + const dpp = input.frozenExpectationSet.derivedPathPatterns; + if (Array.isArray(dpp)) { + const sorted = [...(dpp as string[])].sort(compareCodeUnits); + if (JSON.stringify(dpp) !== JSON.stringify(sorted)) { + findings.push({ check: 'ordering', reason: REASON_ORDER_NOT_COMPARE_CODE_UNITS }); + } + } else { + findings.push({ check: 'ordering', reason: REASON_ORDER_NOT_COMPARE_CODE_UNITS }); + } + + // 3. adequacy — an explicit finding is mandatory (ADR-0020 clause 5(a) / SC-010). + if (input.adequacy?.finding !== 'adequate' && input.adequacy?.finding !== 'inadequate') { + findings.push({ check: 'adequacy', reason: REASON_NO_ADEQUACY }); + } + + return { ok: findings.length === 0, findings }; +} + +async function readJson(path: string): Promise> { + return JSON.parse(await readFile(path, 'utf8')) as Record; +} + +/** Load the live evidence artifacts and the auditor's recorded adequacy finding. */ +export async function auditFromEvidence(evidenceDir: string): Promise { + const frozenExpectationSet = await readJson( + join(evidenceDir, 'frozen-expectations', 'frozen-expectation-set.json'), + ); + const acceptCorpusFreeze = await readJson( + join(evidenceDir, 'accept-corpus-freeze', 'accept-corpus-freeze.json'), + ); + const adequacyAudit = await readJson( + join(evidenceDir, 'accept-corpus-freeze', 'adequacy-audit.json'), + ); + const rawFinding = (adequacyAudit.requirement3_adequacyFinding as Record | undefined) + ?.finding; + // The recorded finding is a prose sentence beginning with the verdict word. + const adequacy: AdequacyFinding = + typeof rawFinding === 'string' && /^ADEQUATE\b/i.test(rawFinding) && !/^INADEQUATE/i.test(rawFinding) + ? { finding: 'adequate' } + : typeof rawFinding === 'string' && /^INADEQUATE\b/i.test(rawFinding) + ? { finding: 'inadequate' } + : {}; + return auditOracleFreeze({ frozenExpectationSet, acceptCorpusFreeze, adequacy }); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const evidenceDir = + process.argv[2] ?? join(process.cwd(), 'specs/010-catalog-backstage/evidence'); + const result = await auditFromEvidence(evidenceDir); + if (result.ok) { + console.log('audit-oracle-freeze: PASS'); + } else { + for (const finding of result.findings) { + console.error(`FAIL [${finding.check}]: ${finding.reason}`); + } + process.exitCode = 1; + } +} diff --git a/scripts/check-freeze-hashes.test.ts b/scripts/check-freeze-hashes.test.ts new file mode 100644 index 00000000..9684c33c --- /dev/null +++ b/scripts/check-freeze-hashes.test.ts @@ -0,0 +1,74 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { cp, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { cleanupTestDir, resetTestDir } from '../packages/core/test/helpers.ts'; +import { checkFreezeHashes, REASON_DRIFT } from './check-freeze-hashes.ts'; + +const DIR_NAME = 'check-freeze-hashes'; +const LIVE_EVIDENCE = join(process.cwd(), 'specs/010-catalog-backstage/evidence'); + +afterEach(async () => { + await cleanupTestDir(DIR_NAME); +}); + +describe('T022 — freeze-hash drift check (R5 mechanism 2)', () => { + test('passes against the live frozen trees, checking exactly the two hashed artifacts', async () => { + const result = await checkFreezeHashes(LIVE_EVIDENCE); + expect(result.ok).toBe(true); + expect(result.findings).toEqual([]); + expect(result.checked).toEqual([ + 'frozen-expectations/frozen-expectation-set.json', + 'accept-corpus-freeze/accept-corpus-freeze.json', + ]); + }); + + test('ignores sibling audit records that carry no contentHash', async () => { + // The live tree contains audit-record.json and adequacy-audit.json; neither is + // in `checked`, proving the drift check does not try to hash sibling records. + const result = await checkFreezeHashes(LIVE_EVIDENCE); + expect(result.checked).not.toContain('frozen-expectations/audit-record.json'); + expect(result.checked).not.toContain('accept-corpus-freeze/adequacy-audit.json'); + }); +}); + +describe('T023 — observed failing: a single mutated byte in a frozen artifact', () => { + test('mutating one byte makes the drift check FAIL with the exact reason; restoring makes it PASS', async () => { + const root = await resetTestDir(DIR_NAME); + const evidence = join(root, 'evidence'); + // Copy only the two freeze dirs into an isolated evidence tree. + await cp(join(LIVE_EVIDENCE, 'frozen-expectations'), join(evidence, 'frozen-expectations'), { + recursive: true, + }); + await cp(join(LIVE_EVIDENCE, 'accept-corpus-freeze'), join(evidence, 'accept-corpus-freeze'), { + recursive: true, + }); + + // Baseline: the untouched copy passes. + expect((await checkFreezeHashes(evidence)).ok).toBe(true); + + // Mutate exactly one byte of one frozen artifact WITHOUT touching its recorded + // contentHash — flip a single character inside a value so the JSON stays parseable. + const target = join(evidence, 'frozen-expectations', 'frozen-expectation-set.json'); + const original = await readFile(target, 'utf8'); + const idx = original.indexOf('workspaces/alpha/src/**'); + expect(idx).toBeGreaterThan(-1); + // Change 'alpha' -> 'blpha' (one byte: 'a' -> 'b') at the located path value. + const mutated = original.slice(0, idx) + 'b' + original.slice(idx + 1); + expect(mutated).not.toBe(original); + expect(mutated.length).toBe(original.length); // exactly one byte changed + await writeFile(target, mutated, 'utf8'); + + const failed = await checkFreezeHashes(evidence); + expect(failed.ok).toBe(false); + const drift = failed.findings.find( + (f) => f.file === 'frozen-expectations/frozen-expectation-set.json', + ); + expect(drift).toBeDefined(); + expect(drift!.reason).toBe(REASON_DRIFT); + expect(drift!.recomputed).not.toBe(drift!.recorded); + + // Restore the byte; the check passes again. + await writeFile(target, original, 'utf8'); + expect((await checkFreezeHashes(evidence)).ok).toBe(true); + }); +}); diff --git a/scripts/check-freeze-hashes.ts b/scripts/check-freeze-hashes.ts new file mode 100644 index 00000000..619107ec --- /dev/null +++ b/scripts/check-freeze-hashes.ts @@ -0,0 +1,107 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { canonicalHash } from './audit-oracle-freeze.ts'; + +/** + * R5 mechanism 2 — the CI freeze-hash drift check. It re-derives the canonical + * content hash of every frozen artifact under the two freeze directories and fails + * the build on any drift from the recorded `contentHash`. Because the freeze is what + * makes the oracle immutable, this is why the freeze artifacts must be git-tracked + * under specs/010-catalog-backstage/evidence/ (plan.md Barrier B, R5). + * + * "Everything under" is interpreted as: every JSON artifact that carries a recorded + * `contentHash`. Sibling audit records (audit-record.json, adequacy-audit.json) carry + * no `contentHash` — hashing them would be circular and is deliberately out of scope + * (evidence/README.md audit-records-as-siblings rule). Any JSON that DOES carry a + * contentHash is checked; any drift, missing hash on a hashed artifact, or unreadable + * file fails the build. + */ + +const FREEZE_DIRS = ['frozen-expectations', 'accept-corpus-freeze'] as const; + +export interface DriftFinding { + file: string; + reason: string; + recorded?: string; + recomputed?: string; +} + +export interface DriftResult { + ok: boolean; + checked: string[]; + findings: DriftFinding[]; +} + +export const REASON_DRIFT = 'recorded contentHash does not match recomputed canonical hash (freeze drift)'; +export const REASON_NOT_OBJECT = 'frozen artifact is not a JSON object'; + +async function listJsonFiles(dir: string): Promise { + let entries: import('node:fs').Dirent[]; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return []; + } + const files: string[] = []; + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.json')) files.push(join(dir, entry.name)); + } + return files.sort(); +} + +export async function checkFreezeHashes(evidenceDir: string): Promise { + const findings: DriftFinding[] = []; + const checked: string[] = []; + + for (const sub of FREEZE_DIRS) { + const dir = join(evidenceDir, sub); + for (const file of await listJsonFiles(dir)) { + const rel = `${sub}/${file.split('/').pop()}`; + let parsed: unknown; + try { + parsed = JSON.parse(await readFile(file, 'utf8')); + } catch { + findings.push({ file: rel, reason: 'frozen artifact is not valid JSON' }); + continue; + } + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + findings.push({ file: rel, reason: REASON_NOT_OBJECT }); + continue; + } + const obj = parsed as Record; + // Only artifacts that carry a recorded contentHash are in scope. Sibling audit + // records (no contentHash) are intentionally skipped, not failed. + if (typeof obj.contentHash !== 'string') continue; + checked.push(rel); + const recomputed = canonicalHash(obj); + if (recomputed !== obj.contentHash) { + findings.push({ + file: rel, + reason: REASON_DRIFT, + recorded: obj.contentHash, + recomputed, + }); + } + } + } + + return { ok: findings.length === 0, checked, findings }; +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const evidenceDir = + process.argv[2] ?? join(process.cwd(), 'specs/010-catalog-backstage/evidence'); + const result = await checkFreezeHashes(evidenceDir); + if (result.ok) { + console.log(`check-freeze-hashes: ok (${result.checked.length} artifacts: ${result.checked.join(', ')})`); + } else { + for (const finding of result.findings) { + const detail = finding.recorded + ? ` recorded=${finding.recorded} recomputed=${finding.recomputed}` + : ''; + console.error(`${finding.file}: ${finding.reason}${detail}`); + } + process.exitCode = 1; + } +} diff --git a/specs/010-catalog-backstage/evidence/README.md b/specs/010-catalog-backstage/evidence/README.md new file mode 100644 index 00000000..fda71c80 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/README.md @@ -0,0 +1,105 @@ +# Barrier B evidence — feature 010 (`010-catalog-backstage`) + +**Status of this tree: `IS THE BARRIER`.** Everything under this directory is the +anti-backfilling control that ADR-0020 clause 5(a) and clause 6 require to exist +*before any generator-derived output exists*. + +Created by task **T013** (`../tasks.md`, Phase B). Populated by **T014–T018**. +Audited independently by **T019–T021**. Hash-drift-checked in CI by **T022–T023**. +Gated by the **T024** checkpoint, which no task in phases E, F, or G may precede. + +--- + +## 1. Why this tree is tracked in git + +`research.md` R5 names three enforcement mechanisms, all required, none sufficient +alone. Mechanism 2 is **hash match**: CI re-derives the content hash of every +frozen artifact and fails the build on drift. CI can only re-derive a hash of a +file it can see, so these artifacts must be **git-tracked**. + +ADR-0015's Condition of Acceptance 1 records the hazard being corrected: spike +009's evidence bundle was untracked and scratch-only, so "nothing in the +repository will stop someone reusing a stale copy." An untracked freeze is not a +freeze. `plan.md` fixes this path (no prior document did), on the authority of R5 +mechanism 2 together with that Condition of Acceptance. + +## 2. Layout + +| Path | Holds | Written by | +| --- | --- | --- | +| `frozen-expectations/` | The re-frozen oracle — `FrozenExpectationSet` (`../data-model.md` §16) | T017 | +| `frozen-expectations/audit-record.json` | The independent audit of the oracle | T019 (**not** T014–T018) | +| `accept-corpus-freeze/` | The clause-5 gate artifact — `AcceptCorpusFreeze` (`../data-model.md` §17) | T014–T016, T018 | +| `accept-corpus-freeze/adequacy-audit.json` | The independent adequacy finding | T019 (**not** T014–T018) | +| `negative-cases/` | Retained failing inputs, per ADR-0016 | T020, T021, T023 | +| `barrier-b-checkpoint.json` | The `BARRIER_B_CLEARED` record | T024 | + +## 3. The content-hash rule + +Every frozen artifact records its own `contentHash`. T019 **recomputes** it from +the artifact rather than copying the recorded value, and T022's CI check +re-derives it on every build. + +`contentHash` is the lowercase-hex **SHA-256** of the artifact's **canonical +form**, defined as: + +1. Take the artifact's top-level JSON object. +2. Remove the `contentHash` key itself. Remove nothing else. +3. Serialize with a deterministic serializer that + - orders every object's keys ascending by `compareCodeUnits` + (`packages/core/src/ordering/index.ts` — `a < b ? -1 : a > b ? 1 : 0` over + UTF-16 code units, never `localeCompare`), + - preserves array element order exactly as stored, + - emits no insignificant whitespace (no spaces, no newlines between tokens), + - escapes strings per `JSON.stringify`, + - emits UTF-8 with **no** trailing newline. +4. SHA-256 that byte sequence; render lowercase hex. + +**Why `contentHash` is the only removal, and why the audit records live in +sibling files.** `../data-model.md` §16 and §17 give both frozen types an +`auditRecord` member. If the audit were written *into* the artifact it audits, +recording the audit would change the artifact's bytes and therefore its hash, and +the recorded hash could never match a re-derivation. The audit records are +therefore separate sibling files (`audit-record.json`, `adequacy-audit.json`), +which is also what T019's own task line specifies. The composed +`FrozenExpectationSet` / `AcceptCorpusFreeze` of the data model is the pair taken +together; the hash covers the frozen half, which is the half that must not move. + +## 4. What is deliberately absent from this tree + +Per R5 **mechanism 1 (input absence)** and R5 **mechanism 3 (ordering)**, and +confirmed by T024, this tree contains and must continue to contain: + +- **No `InputManifest`.** No file here carries `manifestSchemaVersion`, + `requestedSnapshotSchemaVersion`, `requiredCapabilities`, or a `sources` array + of `{path, digestAlgorithm, digest}` records (`../data-model.md` §1). The freeze + artifacts name a corpus by `corpusRef` (repository + commit) and name individual + documents by `sourcePath` + `documentIndexInFile`; that is a record of *what was + frozen*, not an input the generator may read. Absent a manifest, the generator + has no corpus — `input-manifest.md` §5 forbids recursive walking and glob-based + descriptor discovery, so there is no second route to one. +- **No comparison harness.** Nothing here reads generator output, because none + exists. The harness that reads both generator output and these expectations is + authored in Phase F (T087), strictly after this freeze and its audit. +- **No generator output.** No `SnapshotEnvelope`, and no derived-ownership result + for any descriptor-sourced entity — not persisted, not in memory, not asserted + in a test (`research.md` R4). + +## 5. Standing honesty constraints on everything in this tree + +1. **ADR-0014 rung 1 only.** Nothing here is reference-verified (rung 2) or + externally validated (rung 3), and nothing here schedules or prepares a + release. ADR-0020 authorizes the work, not the release. +2. **Only the corpus *data* is third-party.** The descriptors are real and + authored upstream. The overlay, the expected paths, the selection basis, and + the audit are all the maintainer's own. Per ADR-0014's honesty rules, none of + this may be described as external, third-party, or community *validation*. +3. **Integrity is not correctness.** These artifacts fix what the output is + *expected* to be. They do not show that any output is right. That is FR-056 / + SC-011's job, in Phase F, on its own evidence and its own PASS/FAIL. +4. **No claim about Backstage as a running system.** The admissibility warrant + available here is exactly what the four pinned validator predicates return at + Backstage commit `1121a4facd9e321179d0402c3f355e4a649e84d9`. +5. **Every number names where it was read.** Corpus figures come from + `research.md` R14 and are re-derived here; the derivation is recorded in + `accept-corpus-freeze/selection-basis.md` §5. diff --git a/specs/010-catalog-backstage/evidence/accept-corpus-freeze/README.md b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/README.md new file mode 100644 index 00000000..57137e36 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/README.md @@ -0,0 +1,37 @@ +# `accept-corpus-freeze/` — the ADR-0020 clause-5 gate artifact + +Holds the `AcceptCorpusFreeze` of `../../data-model.md` §17. + +| File | Written by | Purpose | +| --- | --- | --- | +| `selection-basis.md` | T014 | How the corpus was chosen, how large, and how the known-failing populations were handled — **recorded before the rule was applied** | +| `overlay.json` | T015 | The maintainer-authored `adrkit.io/owned-paths` overlay | +| `expected-paths.json` | T016 | Expected path matches per canonical id, hand-derived from the frozen contracts | +| `accept-corpus-freeze.json` | T018 | The assembled artifact, frozen in the same cycle | +| `adequacy-audit.json` | **T019** | The independent audit, with its **explicit adequacy finding** | + +## The two things a reader should check first + +1. **`selection-basis.md` was committed before the corpus was enumerated.** + ADR-0020 clause 5 requires the selection basis and size to be "fixed and + recorded in that same cycle, **not chosen afterwards**". Selecting entities and + then writing down a rationale that fits them is the precise failure mode the + clause forecloses, and prose alone cannot distinguish the two. The ordering is + therefore recorded in git history rather than asserted: see `selection-basis.md` + §7, which names the commit that fixed the rule and the commit that applied it. + +2. **The audit reaches adequacy, not merely integrity.** ADR-0020 clause 5 and + SC-010 are explicit: "an audit that passes on integrity without reaching + adequacy **does not satisfy this clause**". `adequacy-audit.json` must carry an + `adequacyFinding` of `"adequate-for-the-claim"` or `"inadequate"`, with a + reason. A run that confirms the hashes and stops is a **FAIL**, and T021 + retains exactly that run as a permanent negative case. + +## What this freeze does and does not buy + +Carried from ADR-0020 clause 5 so it is not overstated downstream. It exercises +ownership derivation against real descriptor structure and real field shapes, at +whatever scale the audited corpus fixes. It gates **technical compatibility +only**. It does **not** evidence that the mapping reflects anyone's actual +ownership, that anyone else wants the annotation, or that adoption risk has +fallen. diff --git a/specs/010-catalog-backstage/evidence/accept-corpus-freeze/accept-corpus-freeze.json b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/accept-corpus-freeze.json new file mode 100644 index 00000000..6e7e9058 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/accept-corpus-freeze.json @@ -0,0 +1,447 @@ +{ + "//": "T018 — AcceptCorpusFreeze, data-model.md §17. The ADR-0020 clause-5 gate artifact, assembled in the SAME CYCLE as T014 (selection basis and size), T015 (overlay), T016 (expected paths) and T017 (the re-frozen oracle). This artifact and the T017 oracle are frozen together or not at all.", + "task": "T018", + "barrierSide": "IS THE BARRIER", + "discharges": [ + "FR-054 (same-cycle freeze)" + ], + "frozenAt": "2026-08-05T03:43:58Z", + "corpusRef": { + "repository": "github.com/backstage/community-plugins", + "commit": "92e9e4e09c76cc57f3475029b73e5ec84498a459" + }, + "selectionBasis": "Recorded in full at accept-corpus-freeze/selection-basis.md, and committed BEFORE it was applied (that file, §7). In summary: over all 167 entity documents of the pinned corpus, a document satisfies P when its file basename is exactly `catalog-info.yaml`; it parses as YAML with uniqueKeys and zero parse errors; it is entity-shaped; it is admissible under ADR-0015's four field validators as reproduced at spec.md FR-016 and pinned to Backstage commit 1121a4facd9e321179d0402c3f355e4a649e84d9; and its canonical id (entity-identity.md §1) is unique across all admissible documents of the corpus, with EVERY member of any colliding group excluded rather than one member kept — keeping one would be last-wins resolution, which entity-identity.md §3 forbids, moved earlier so it is harder to see. The corpus is then one entity per workspace: workspaces ordered ascending by compareCodeUnits, a workspace eligible when at least one of its documents satisfies P, and from each eligible workspace the compareCodeUnits-least satisfying document by sourcePath with documentIndexInFile ascending as tiebreak — taken from the first 24 eligible workspaces. 158 of 167 documents satisfy P; 79 workspaces are eligible; 24 are selected.", + "size": 24, + "sizeIsNotAMinimum": "ADR-0012 holds that production limits are not guessed now but ratified from evidence, and ADR-0020 clause 5 declines to fix a minimum entity count for that reason; spec.md FR-055 forbids the specification or the implementation inventing one. 24 is the size of THIS frozen corpus. It is not a minimum, not a threshold, and not a production limit, and nothing here ratifies a scale bound. Whether 24 is adequate is the independent auditor's explicit finding to make at T019, not this artifact's to assert.", + "corpusFacts": { + "note": "Each figure was re-derived from the pin by applying FR-016's four validator predicates and entity-identity.md §1 directly, and each reproduced research.md R14 exactly. None is asserted from R14 without re-derivation, and none was adjusted to fit.", + "descriptorFilesExactBasename": 156, + "entityDocuments": 167, + "filesCarryingAnyAnnotations": 23, + "documentsCarryingOwnedPathsAnnotationUpstream": 0, + "unsubstitutedSkeletonFiles": 5, + "documentsWithInvalidMetadataName": { + "total": 7, + "failingOnCharacterClass": 5, + "failingOnLengthAlone": 2 + }, + "twoPopulationsThatAreNotOnePopulation": "\"Over 63 characters\" and \"invalid\" are different sets (research.md R14; contracts/admissibility.md §2.1). Of the 7, five fail on character class and two fail on length ALONE. Any figure that collapses them is wrong even when its total is right.", + "documentsFailingP4Inadmissible": 7, + "documentsFailingP5CollidingCanonicalId": 2, + "documentsSatisfyingP": 158, + "eligibleWorkspaces": 79, + "excludedPlaceholderPopulation": "All 5 unsubstituted skeleton descriptors carry `${{ values.name | dump }}` as metadata.name, which fails isValidObjectName. They are excluded on INADMISSIBILITY, not on collision: contracts/admissibility.md §4.1 is explicit that an inadmissible descriptor never acquires a canonical id and so can never participate in a duplicate determination in either direction, and spec.md FR-020 fixes inadmissibility as the earlier and more specific defect.", + "excludedResidualValidDuplicate": "workspaces/nexus-repository-manager/plugins/nexus-repository-manager/catalog-info.yaml holds two documents, both fully admissible, both canonicalizing to component:default/backstage-community-nexus-repository-manager. This is the pair ADR-0020 names as the reason spike 009's SC-010 was unsatisfiable under the frozen inputs, and the residual valid duplicate ADR-0015 Condition of Acceptance 3 says no admissibility rule can or should reach. BOTH documents are excluded, never one. That workspace has no other qualifying document and is therefore not eligible, contributing no entity. The corpus is free of duplicate canonical ids because this pair was identified and excluded, not because the corpus happened to be clean.", + "whatExclusionDoesNotMean": "Excluding a descriptor from this accept corpus is a statement about this freeze and nothing else. It is not a claim the descriptor is defective, nor licence for the generator to skip anything at run time — spec.md FR-018 requires an inadmissible descriptor to abort the entire operation, never to be skipped, filtered, downgraded, or set aside so the remainder of a batch can succeed." + }, + "sameCycleAttestation": { + "requirement": "ADR-0020 clause 5 requires the corpus, its overlay, its expected paths, and its selection basis and size to be frozen within the same T014 → T014a cycle, before any generator output; SC-010 and tasks.md T018 repeat that this artifact and the T017 oracle are frozen together or not at all.", + "howItWasSatisfied": "T014 through T018 were authored in one uninterrupted session and landed across exactly two commits. The first commit contains ONLY the rule (selection-basis.md §1–§6, naming no entity, no canonical id and no sourcePath) plus the T013 evidence tree. The second commit — the commit that adds this file — contains the enumeration and every artifact derived from the rule, and does not modify §1–§6.", + "ruleCommit": "654031b3ef84f3a625bc8d06d274c4ae83056b84", + "freezeCommit": "The commit that adds this file. Resolve it with: git log --diff-filter=A --format=%H -- specs/010-catalog-backstage/evidence/accept-corpus-freeze/accept-corpus-freeze.json", + "whatTheAuditorShouldCheck": "That `git diff -- .../selection-basis.md` shows additions only, none of them above §8. Any change to §1–§6 between those commits invalidates this freeze, and the correct response is to redo the cycle rather than amend the record.", + "noGeneratorExisted": "At neither commit does packages/adapters/catalog-backstage exist, and no generator has been written, built, or run. No SnapshotEnvelope exists. No derived-ownership result for any descriptor-sourced entity exists — not persisted, not in memory, not asserted in a test (research.md R4). No input manifest exists anywhere in the tree, and no comparison harness exists anywhere in the repository." + }, + "overlay": [ + { + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[]" + } + ], + "overlayProvenance": { + "authorship": "maintainer-authored", + "entries": 23, + "ordering": "Ascending `compareCodeUnits` on `sourcePath`, with `documentIndexInFile` ascending as tiebreak. For this corpus that order coincides with the frozen selection order of selection-basis.md §3, because each of the 24 selected entities comes from a distinct workspace and no selected workspace name is a strict prefix of another.", + "statedPlainly": "Zero descriptors in the pinned corpus carry adrkit.io/owned-paths — R14 records it and this freeze re-derived it. Every annotation value is the maintainer's own, written over real upstream descriptors left otherwise unmodified. ADR-0020 clause 5 adopts ADR-0012 gate 3's named construction, \"synthetic explicit annotations over pinned public corpora under independent adversarial review\"; the independent adversarial review half is T019. Only the corpus data is third-party — never the validation (ADR-0014 honesty rules).", + "ownershipStatesExercised": { + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1 + }, + "clause5Floor": "Clause 5 requires at least one non-empty annotation on a real entity; this overlay carries 22, so the all-annotation-absent corpus clause 5 warns satisfies nothing is not what was frozen." + }, + "expectedPaths": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation IS present and IS a string scalar; it decodes via JSON.parse to an array of length zero. owned-paths-annotation.md §3 requires this to be a decoded-value check, never a raw-string equality check, and §4 requires it not to be confused with a single empty-string element, which would instead be rejected at the glob dialect's `empty` rule." + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "sourcePath": "workspaces/cost-insights/plugins/cost-insights-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation key is wholly absent — there is no overlay entry for this entity. owned-paths-annotation.md §1 step 1 decides presence by an explicit discriminant, never by inferring it from a raw value being undefined, and §3's non-conflation rule forbids treating this as equivalent to the explicit-empty entity above." + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**" + ] + } + ], + "expectedPathsProvenance": { + "entries": 24, + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "howProduced": "By hand, by applying the frozen contracts to the maintainer-authored overlay values in overlay.json. Nothing here was produced by, checked against, or adjusted to match any generator. No generator exists at this point: no package has been created, and Phase E may not begin until T024.", + "disclosedArithmeticCheck": "Each `expectedPaths` array below was written by hand and then checked against `compareCodeUnits` — whose entire definition is `a < b ? -1 : a > b ? 1 : 0` over UTF-16 code units (packages/core/src/ordering/index.ts) — to confirm the hand ordering was not miscounted. This is disclosed rather than done silently. It is not generator output under research.md R4: a two-line comparator promoted into `@adrkit/core` is a frozen repository primitive, not the assembled generator, and it computes no ownership result. All eleven arrays and the 25-element union matched the hand-written values on the first check.", + "agreementWithTheOracle": "Every (canonicalId, expectedPaths) pair here is identical to the corresponding entry in frozen-expectations/frozen-expectation-set.json. The two artifacts are two views of one freeze, and a divergence between them is a freeze failure, not a discrepancy to be reconciled." + }, + "contentHash": "f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca", + "contentHashNotes": { + "algorithm": "Lowercase-hex SHA-256 over the canonical form defined in ../README.md §3: this top-level object with the `contentHash` key removed and nothing else removed, keys ordered ascending by compareCodeUnits, array order preserved, no insignificant whitespace, UTF-8, no trailing newline.", + "forTheAuditor": "Recompute it from this artifact. Do not copy it. data-model.md §16: \"An audit that transcribes the author's declared hash has verified nothing.\"" + }, + "warrantAndLimits": { + "whatThisFreezeBuys": "It fixes, in advance and in the open, what ownership derivation over this corpus is expected to produce. Per ADR-0020 clause 5 it exercises derivation against real descriptor structure and real field shapes, and it gates TECHNICAL COMPATIBILITY ONLY.", + "whatItDoesNotBuy": [ + "It does not evidence that the mapping reflects anyone's actual ownership.", + "It does not evidence that anyone else wants the annotation, and adoption remains entirely ungated.", + "It does not evidence that adoption risk has fallen.", + "It is not correctness evidence. A populated, digest-verified envelope would prove INTEGRITY, NOT CORRECTNESS — a semantically wrong envelope can carry a perfectly valid self-digest (ADR-0020 clause 5; spec.md FR-058; SC-012). Correctness is claimed only on FR-056 / SC-011, in Phase F, recording its own hashes and its own PASS/FAIL and inheriting nothing from this step.", + "It says nothing about Backstage as a running system. The admissibility warrant is exactly what the four pinned validator predicates return at Backstage commit 1121a4facd9e321179d0402c3f355e4a649e84d9.", + "ADR-0014 rung 1 only — not reference-verified, not externally validated, and no release is scheduled, prepared, or implied." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/accept-corpus-freeze/adequacy-audit.json b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/adequacy-audit.json new file mode 100644 index 00000000..ed634e4d --- /dev/null +++ b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/adequacy-audit.json @@ -0,0 +1,92 @@ +{ + "artifact": "adequacy-audit", + "schemaNote": "Independent T019 auditor adequacy finding for the accept corpus freeze (T018), plus the consolidated auditor verdict. Sibling to accept-corpus-freeze.json; deliberately NOT part of its canonical hash input.", + "task": "T019 (feature 010, Barrier B, ADR-0020 clause 6 oracle cycle, step T014a)", + "auditor": { + "role": "independent reviewer", + "authoringInvolvementInT014ToT018": false, + "baseCommitAudited": "4fff13d" + }, + "requirement3_adequacyFinding": { + "mandate": "ADR-0020 clause 5(a) requires an explicit adequacy judgement on the accept corpus; an integrity/hash confirmation alone does not satisfy it. The authoring session fixed size = 24 (one entity per eligible workspace, first 24 of 79 eligible workspaces in compareCodeUnits order) and stated its reasoning. This auditor judges that reasoning.", + "claimTheCorpusGates": "Technical compatibility only (ADR-0020 clause 5 step (a)): that the ownership-derivation contract can be exercised end-to-end against real Backstage descriptor structure and that the frozen oracle's ordering and dedup behaviour are the contract-admissible ones. The freeze explicitly does NOT claim scale ratification (ADR-0012 / FR-055), correctness of the generator, SC-010 satisfaction, or adoption. The adequacy judgement is scoped to that narrow claim.", + "coverageVerifiedIndependently": { + "ownershipStates": { + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1, + "note": "All three ownership states from the ownership model are present. explicit-paths carries >=2 instances (22); explicit-empty and annotation-absent carry exactly 1 each." + }, + "globFeatureCatalogue": "Positions 0-21 (the explicit-paths entities, in frozen selection order) realise 11 distinct deduped expectedPaths arrays, each carried by exactly two distinct canonical ids (a mod-11 assignment repeated across positions 0-10 and 11-21). Independently recomputed: exactly 11 distinct arrays, every one appearing exactly twice. Each documented ordering/dedup trap therefore has >=2 independent instances: uppercase-vs-lowercase (src/Utils vs src/utils), punctuation ordering (a/b, a-b, a., a/ family), duplicate-collapse (packages/core/** listed twice; docs/** repeated), numeric-vs-lexical version segments (v2/v10/v_next), and the bare '**' catch-all.", + "overlapNoExclusiveWinner": "The (acr, bazaar) pair share identical owned paths under distinct canonical ids, exercising entity-identity.md \u00a74's 'no exclusive winner' overlap case; the mod-11 mirroring gives 11 such overlap instances rather than one.", + "overlayEntityReconciliation": "23 overlay rows vs 24 entities reconciles exactly: 22 explicit-paths + 1 explicit-empty carry an annotation value (23 overlay rows); the single annotation-absent entity (cost-insights-common) has no annotation to overlay and correctly carries no overlay row. Not a defect." + }, + "reasoningJudged": { + "upperBound": "Reviewability. Expected paths are hand-authored so they can be checked line-by-line by a reviewer; a materially larger corpus would force code-derived expectations, which clause 6 constrains. This auditor accepts the upper bound as sound for a hand-verifiable oracle.", + "lowerBound": "Coverage-with-repetition: every ownership state and every catalogued glob/dedup feature exercised with >=2 independent instances so that a single transcription slip cannot silently pass. This auditor independently confirmed the >=2 repetition holds for all 11 catalogue entries and for each ordering trap. The lower bound is met by the data, not merely asserted.", + "selectionDeterminism": "Selection is 'first 24 of 79 eligible workspaces in compareCodeUnits order' \u2014 a deterministic, restatable rule fixed before entities were known (see pre-commitment finding below), not a hand-picked convenience sample." + }, + "finding": "ADEQUATE for the technical-compatibility claim it gates (ADR-0020 clause 5 step (a)).", + "reasoning": "Size 24 is adequate because it simultaneously (1) covers all three ownership states, (2) exercises every documented glob ordering/dedup trap with at least two independent instances, (3) provides 11 independent overlap/no-exclusive-winner instances, and (4) is selected by a deterministic, pre-committed rule rather than a convenience sample \u2014 while remaining small enough to be verified by hand, which the upper bound requires. The corpus is fit for the narrow purpose of demonstrating the contract runs against real descriptor structure and that the oracle's ordering is contract-admissible. This auditor did NOT invent a minimum count; per ADR-0012 and FR-055 a production/scale limit must be ratified from evidence, and this freeze explicitly disclaims making any scale-ratification claim. The adequacy judgement is therefore that 24 is sufficient for what is claimed and no more, NOT that 24 is a ratified production sample size.", + "boundedLimitation": "The pinned Backstage corpus (github.com/backstage/community-plugins @ 92e9e4e09c76cc57f3475029b73e5ec84498a459) is NOT checked out in this repository. This auditor could therefore NOT re-run the four FR-016 validator predicates or entity-identity canonicalisation against the actual descriptor files. The funnel figures (167 documents / 7 inadmissible / 2 colliding / 158 satisfying P / 79 eligible) were verified for internal arithmetic consistency, for agreement with research.md R14 and admissibility.md, and for cross-artifact consistency \u2014 but their ground truth against the pin is accepted on the authoring session's disclosed re-derivation, not independently reproduced. This is a bounded, disclosed limitation of the audit, not a defect in the freeze. It does not affect the adequacy finding, which turns on the structure and coverage of the 24 selected entities (fully inspectable here), not on the exact upstream funnel counts." + }, + "funnelReDerivation": { + "entityDocuments": 167, + "inadmissibleP4": 7, + "collidingP5": 2, + "satisfyingP": 158, + "arithmetic_167_minus_7_minus_2_equals_158": true, + "eligibleWorkspaces": 79, + "selectedCount": 24, + "selected_le_eligible": true, + "invalidNameSplitPreserved": "5 fail on character class + 2 fail on length alone = 7 documents with invalid metadata.name; the two populations ('invalid' vs 'over 63 chars') are kept distinct and NOT collapsed, matching research.md R14 and admissibility.md \u00a72.1.", + "crossArtifactAgreement": "All 24 entities' expectedPaths arrays are byte-identical between accept-corpus-freeze.json and frozen-expectation-set.json (0 missing, 0 mismatch), matched by canonicalId.", + "verdict": "PASS \u2014 internally consistent and cross-artifact consistent; ground-truth-against-pin not independently reproduced (see boundedLimitation)." + }, + "exclusionReasoning": { + "fivePlaceholders": { + "claim": "The 5 unsubstituted skeleton descriptors are excluded on INADMISSIBILITY, not collision \u2014 they carry '${{ values.name | dump }}' as metadata.name, fail isValidObjectName, and never acquire a canonical id.", + "checkedAgainst": "admissibility.md \u00a74.1 (an inadmissible descriptor never acquires a canonical id and cannot participate in a duplicate determination) and spec.md FR-020 (inadmissibility is the earlier, more specific defect). The 5 placeholders are a subset of the 7 P4-inadmissible documents.", + "verdict": "CONSISTENT." + }, + "nexusPair": { + "claim": "The Nexus pair (workspaces/nexus-repository-manager/.../catalog-info.yaml, two documents both canonicalising to component:default/backstage-community-nexus-repository-manager) is the residual VALID duplicate; BOTH documents are excluded, and that workspace contributes no entity.", + "checkedAgainst": "ADR-0015 Condition of Acceptance 3 (the irreducible valid duplicate that no admissibility rule can or should reach) and ADR-0020's identification of this pair as why spike-009 SC-010 was unsatisfiable under frozen inputs. Both documents are admissible, so exclusion is by collision (P5), not inadmissibility \u2014 correctly the opposite basis from the placeholders.", + "verdict": "CONSISTENT \u2014 the two exclusion mechanisms are correctly distinguished: placeholders never reach canonicalisation; the Nexus pair does and collides." + } + }, + "supplementaryChecks": { + "preCommitment": { + "claim": "Commit 654031b contains only the selection RULE (naming no selected entity); commit 4fff13d adds selection-basis.md \u00a78 with 124 insertions, 0 deletions, proving \u00a71-\u00a77 were untouched after entities were known.", + "verifiedBy": "git diff --numstat 654031b 4fff13d on selection-basis.md => 124 insertions, 0 deletions; the added block is entirely \u00a78; the rule commit names no selected canonical id.", + "verdict": "CONFIRMED independently." + }, + "barrierClaim": { + "claim": "No generator was run: no catalog-backstage adapter package, no generator-derived output, no SnapshotEnvelope, no input manifest, no comparison harness exists.", + "verifiedBy": "Inspecting the tree, not the assertion: no catalog-backstage package under packages/; no SnapshotEnvelope or InputManifest fields present anywhere under specs/010-catalog-backstage/evidence/; no comparison harness.", + "verdict": "CONFIRMED independently." + }, + "toolchain": { + "bunTest": "857 pass, 0 fail (after bun install \u2014 see note)", + "typecheck": "exit 0", + "adrLint": "checked 20 records, 0 errors, 0 warnings, exit 0", + "note": "On first run bun test showed 123 failures; root-caused to the worktree never having had `bun install` run (node_modules/@adrkit absent, CLI failing with 'Cannot find module @adrkit/core'). The tree was clean (no auditor changes) at that point, so this was purely an uninitialised-workspace state, not a defect in the artifacts and not caused by this audit. After `bun install` all 857 tests pass." + } + }, + "requirement4_auditorVerdict": { + "verdict": "PASS", + "inTheAuditorsWords": "As the independent T019 reviewer, I find the Barrier B oracle freeze sound for the technical-compatibility claim it makes. Both content hashes recompute exactly from the artifact bytes using a serializer I wrote from scratch (e641ae5e...c695c2430c and f98e6d46...c9294aca), so the recorded values are honest and the freeze is byte-stable. derivedPathPatterns is in compareCodeUnits order, not input order \u2014 the spike-009 defect is absent \u2014 and I confirmed this against the frozen comparator's actual semantics, including that a locale-aware sort would give a different, wrong answer for the deliberately-present case and punctuation data. The corpus of 24 is ADEQUATE for the narrow technical-compatibility claim: it covers every ownership state and every documented glob/dedup trap with at least two independent instances under a deterministic, pre-committed selection rule, while staying hand-verifiable. The pre-commitment and no-generator-run claims both hold under direct inspection. The one honest limitation is that the pinned upstream corpus is not checked out here, so the funnel counts are verified for consistency but not reproduced against ground truth; this does not affect the adequacy finding. I found no defect in the audited artifacts. I record PASS.", + "explicitNonClaims": [ + "This PASS does NOT clear Barrier B: T020-T024 of the oracle cycle remain.", + "This PASS does NOT assert ADR-0014 rung 2 or rung 3.", + "This PASS does NOT assert SC-010 is satisfied beyond the technical-compatibility step this freeze gates.", + "This PASS does NOT ratify 24 (or any number) as a production/scale sample size \u2014 no minimum count was invented, per ADR-0012 and FR-055." + ], + "defectsFound": [], + "concernsAndLimitations": [ + "BOUNDED LIMITATION (disclosed): the pinned Backstage corpus is not checked out, so the four validator predicates and canonicalisation could not be re-run against ground truth; funnel figures were verified for internal/cross-artifact consistency only.", + "RESIDUAL NON-FALSIFIABILITY (clause 6): the disclosed 'hand-derived then checked against compareCodeUnits' process is not falsifiable from the artifact bytes alone; judged CONSISTENT with clause 6 because the admissible ordering is uniquely fixed by the owned-paths-annotation.md \u00a73 contract, leaving no room to game the oracle, so the gaming risk the clause guards against is nil regardless.", + "ENVIRONMENTAL (not a defect): the worktree required `bun install` before the test suite would pass; recorded for reproducibility." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/accept-corpus-freeze/expected-paths.json b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/expected-paths.json new file mode 100644 index 00000000..f221878e --- /dev/null +++ b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/expected-paths.json @@ -0,0 +1,207 @@ +{ + "//": "T016 — expected path matches per canonical id, hand-derived from the frozen contracts. The `expectedPaths` array below is exactly the `expectedPaths` member of AcceptCorpusFreeze (data-model.md §17) and is embedded verbatim in accept-corpus-freeze.json.", + "task": "T016", + "barrierSide": "IS THE BARRIER", + "discharges": ["FR-054 (expected-paths half)"], + "derivation": { + "howTheseWereProduced": "By hand, by applying the frozen contracts to the maintainer-authored overlay values in overlay.json. Nothing here was produced by, checked against, or adjusted to match any generator. No generator exists at this point: no package has been created, and Phase E may not begin until T024.", + "rulesApplied": [ + "owned-paths-annotation.md §1 — decode-then-validate order: presence discriminant, then string-scalar check, then JSON.parse, then array shape, then per-pattern validation. Every annotationValue in overlay.json is a present YAML string scalar carrying well-formed JSON that decodes to an array of strings, so every one of them reaches per-pattern validation.", + "glob-dialect.md §3 — the fifteen ordered validation rules. Every pattern is POSIX segments over `A-Za-z0-9_-.` plus `*`, whole-segment `**`, and `?`; none has a leading `/` or `!`, a backslash, a brace, a bracket, a parenthesis, a comma, a control character, a `.` or `..` segment, an empty segment, a disallowed character, or a `**` sharing a segment with other characters. All therefore reach rule 15 and are `accepted`.", + "owned-paths-annotation.md §3 — `explicit-paths` requires the resulting array to be sorted by `compareCodeUnits` and deduplicated. That sort-and-dedupe is the whole of the transformation between an overlay `annotationValue` and the `expectedPaths` recorded here.", + "entity-identity.md §1 — canonical id is `kind:namespace/name` with the namespace defaulted to `default` when omitted, then the ENTIRE string lowercased." + ], + "disclosedArithmeticCheck": "Each `expectedPaths` array below was written by hand and then checked against `compareCodeUnits` — whose entire definition is `a < b ? -1 : a > b ? 1 : 0` over UTF-16 code units (packages/core/src/ordering/index.ts) — to confirm the hand ordering was not miscounted. This is disclosed rather than done silently. It is not generator output under research.md R4: a two-line comparator promoted into `@adrkit/core` is a frozen repository primitive, not the assembled generator, and it computes no ownership result. All eleven arrays and the 25-element union matched the hand-written values on the first check.", + "orderingTrapsDeliberatelyIncluded": "`-` (0x2D) < `.` (0x2E) < `/` (0x2F), so `a-b/**` and `a.b/**` precede `a/**`; `*` (0x2A) precedes every letter, so `packages/core/**` precedes `packages/core/src/**`; uppercase precedes lowercase, so `src/Utils/**` precedes `src/utils/**`; digits sort lexically, so `plugins/v10/**` precedes `plugins/v2/**`. Each of these is a case where input order and `compareCodeUnits` order visibly disagree — which is the point, since recording input order is the defect this cycle exists to correct." + }, + "ownershipStateIsCarriedExplicitly": "owned-paths-annotation.md §3's non-conflation rule requires each entity's record to carry the discriminator as its own explicit field, because `explicit-empty` and `annotation-absent` both yield an empty array and MUST NOT be inferred from `expectedPaths` alone. `ownershipState` below is therefore required by a frozen contract, not decoration.", + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "coverage": { + "entities": 24, + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1, + "distinctPatternsAcrossCorpus": 25, + "overlappingPairs": "Eleven pairs of distinct canonical ids share an identical expectedPaths array, and further partial overlaps exist across catalogue entries (`packages/core/**` between C1 and C6; `docs/**` between C3 and C9; `.github/**` between C2 and C8). entity-identity.md §4 requires overlapping owned paths between distinct canonical ids to derive successfully and never to be treated as a collision; this corpus supplies eleven independent instances of that case rather than one." + }, + "expectedPaths": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["packages/cli/**", "packages/core/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["**", ".github/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["docs/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["src/README.md", "src/Utils/**", "src/utils/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["scripts/*.test.ts", "scripts/build-?.ts", "scripts/build.ts"] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["packages/core/**", "packages/core/src/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["plugins/v10/**", "plugins/v2/**", "plugins/v_next/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [".github/**", ".github/workflows/**", "Makefile"] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["docs/**", "examples/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["a-b/**", "a.b/**", "a/**", "a/b/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["**", ".github/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["src/README.md", "src/Utils/**", "src/utils/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["scripts/*.test.ts", "scripts/build-?.ts", "scripts/build.ts"] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["packages/core/**", "packages/core/src/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["plugins/v10/**", "plugins/v2/**", "plugins/v_next/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [".github/**", ".github/workflows/**", "Makefile"] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["docs/**", "examples/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["a-b/**", "a.b/**", "a/**", "a/b/**"] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation IS present and IS a string scalar; it decodes via JSON.parse to an array of length zero. owned-paths-annotation.md §3 requires this to be a decoded-value check, never a raw-string equality check, and §4 requires it not to be confused with a single empty-string element, which would instead be rejected at the glob dialect's `empty` rule." + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "sourcePath": "workspaces/cost-insights/plugins/cost-insights-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation key is wholly absent — there is no overlay entry for this entity. owned-paths-annotation.md §1 step 1 decides presence by an explicit discriminant, never by inferring it from a raw value being undefined, and §3's non-conflation rule forbids treating this as equivalent to the explicit-empty entity above." + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["packages/cli/**", "packages/core/**"] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": ["docs/**"] + } + ] +} diff --git a/specs/010-catalog-backstage/evidence/accept-corpus-freeze/overlay.json b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/overlay.json new file mode 100644 index 00000000..aa34ca63 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/overlay.json @@ -0,0 +1,145 @@ +{ + "//": "T015 — the maintainer-authored adrkit.io/owned-paths overlay for the clause-5 accept corpus. The `overlay` array below is exactly the `overlay` member of AcceptCorpusFreeze (data-model.md §17) and is embedded verbatim in accept-corpus-freeze.json.", + "task": "T015", + "barrierSide": "IS THE BARRIER", + "discharges": ["FR-054 (overlay half)"], + "provenance": { + "annotationAuthorship": "maintainer-authored", + "descriptorAuthorship": "third-party, authored upstream, left otherwise unmodified", + "statedPlainly": "No descriptor in the pinned corpus carries `adrkit.io/owned-paths`. research.md R14 records the count as zero and this freeze re-derived it as zero. Every annotation value below was written by the maintainer. None was read from upstream, and none may be described as an upstream, third-party, external, or community annotation.", + "authority": "ADR-0020 clause 5 adopts ADR-0012 gate 3's named construction — 'synthetic explicit annotations over pinned public corpora under independent adversarial review'. The independent adversarial review half is T019, not this file.", + "notDerivable": "These values are authored data. No adapter code path computes them, and none may. spec.md FR-061 / ADR-0020 clause 7 forbid descriptor-parent, repository-root, and identity-only normalization from appearing in the adapter as inferred, authoritative, default, or opt-in ownership behavior. The assignment rule in selection-basis.md §4 is positional precisely so that it cannot be mistaken for such an inference: an entity's annotation is a function of its index in a frozen order, never of its descriptor's location.", + "notAClaimOfOwnership": "ADR-0020 clause 5 is explicit that this construction gates technical compatibility only. Nothing here evidences that any listed path is a path anyone actually owns." + }, + "assignment": { + "rule": "selection-basis.md §4 — positions 0–21 receive catalogue entry C[i mod 11] in the catalogue's authored input order; position 22 receives the `explicit-empty` value `[]`; position 23 receives no overlay entry at all.", + "positionsCovered": "0–22", + "positionWithoutAnEntry": 23, + "whyPosition23HasNoEntry": "`annotation-absent` means the key is wholly absent from metadata.annotations. owned-paths-annotation.md §1 step 1 decides presence by an explicit discriminant, never by inferring it from a raw value being undefined — so an overlay entry carrying an empty or null annotationValue would be the wrong construction. Absence is represented by absence.", + "ownershipStatesExercised": { + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1 + }, + "whyAllThree": "owned-paths-annotation.md §3's non-conflation rule requires `explicit-empty` and `annotation-absent` to remain distinguishable even though both yield an empty derivedPaths array. A corpus carrying only one of them cannot demonstrate that they were not fused.", + "clause5Floor": "ADR-0020 clause 5 requires at least one non-empty annotation on a real entity. This overlay carries 22, and is therefore not the all-`annotation-absent` corpus clause 5 warns satisfies nothing." + }, + "ordering": "Ascending `compareCodeUnits` on `sourcePath`, with `documentIndexInFile` ascending as tiebreak. For this corpus that order coincides with the frozen selection order of selection-basis.md §3, because each of the 24 selected entities comes from a distinct workspace and no selected workspace name is a strict prefix of another.", + "overlay": [ + { + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[]" + } + ] +} diff --git a/specs/010-catalog-backstage/evidence/accept-corpus-freeze/selection-basis.md b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/selection-basis.md new file mode 100644 index 00000000..ecb445a2 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/accept-corpus-freeze/selection-basis.md @@ -0,0 +1,426 @@ +# Accept-corpus selection basis and size (T014) + +**Task**: T014 · **Barrier side**: `IS THE BARRIER` · **Discharges**: FR-055 +**Normative sources**: ADR-0020 clause 5; ADR-0012 gate 3; `../../spec.md` FR-054, +FR-055; `../../research.md` R14; `../../data-model.md` §17; +`../../contracts/admissibility.md`; +`specs/009-catalog-binding-viability/contracts/entity-identity.md`; +`specs/009-catalog-binding-viability/contracts/glob-dialect.md`; +`specs/009-catalog-binding-viability/contracts/owned-paths-annotation.md`. + +> **Read §7 first if you are auditing this.** ADR-0020 clause 5 requires the +> selection basis and size to be "fixed and recorded in that same cycle, **not +> chosen afterwards**". Prose cannot distinguish a rule written first from a +> rationale written to fit entities already seen. §7 records that distinction in +> git history instead, where it can be checked. + +--- + +## 1. The corpus this freeze draws from + +| Field | Value | +| --- | --- | +| `corpusRef.repository` | `github.com/backstage/community-plugins` | +| `corpusRef.commit` | `92e9e4e09c76cc57f3475029b73e5ec84498a459` | + +**Why this repository and this pin.** `../../research.md` R14 records the full +population facts for exactly two pinned corpora, and this is one of them. Its +counts, its invalid-`metadata.name` population, and its unsubstituted-skeleton +population are all already frozen in a document this feature may not edit, so the +selection rule below can be stated against facts that were fixed before this task +existed rather than against facts discovered while choosing. + +The descriptors are **real, authored upstream, and left otherwise unmodified** +(ADR-0020 clause 5). Only the corpus *data* is third-party. The overlay, the +expected paths, this selection basis, and the audit are the maintainer's own, and +none of them may be described as external, third-party, or community validation +(ADR-0014 honesty rules; ADR-0014 rung 1 only). + +## 2. The admissibility-and-uniqueness predicate `P` + +`P` is evaluated over **every entity document of the pinned corpus**, not over a +pre-filtered subset. A document satisfies `P` when all five hold: + +1. **Basename.** Its file's basename is exactly `catalog-info.yaml`. R14 attaches + this qualification explicitly: a looser path-suffix match over-counts. +2. **Parse.** The file parses as YAML with `uniqueKeys: true` and the document + reports zero parse errors. This is the guard against `duplicate-yaml-key` + (`entity-identity.md` §3), which is a fatal whole-operation trigger. +3. **Shape.** The document is a mapping carrying `apiVersion`, `kind`, and + `metadata.name` — i.e. it is an entity document rather than an empty or + non-entity document. +4. **Admissible under ADR-0015.** All four field validator predicates in + `../../spec.md` FR-016's reproduced table return true, at Backstage commit + `1121a4facd9e321179d0402c3f355e4a649e84d9`: + `isValidApiVersion`, `isValidKind`, `isValidEntityName`, and — only when + `metadata.namespace` is present — `isValidNamespace`. +5. **Canonically unique corpus-wide.** Its canonical id + (`entity-identity.md` §1: default the namespace, then lowercase the **entire** + `kind:namespace/name` string) is unique across **all** admissible documents of + the pinned corpus. + +### 2.1 Why criterion 5 excludes *both* members of a collision + +`entity-identity.md` §3 forbids resolving a canonical-id collision by first-wins +or last-wins. Keeping one member of a colliding pair and discarding the other +would be last-wins applied at selection time instead of at derivation time — the +same prohibited resolution, moved earlier so it is harder to see. Every member of +any colliding group is therefore excluded. + +## 3. The selection rule `S`, and the size + +Let the **workspace** of a document be the path segment immediately below +`workspaces/`. + +1. Order workspace names ascending by `compareCodeUnits` + (`packages/core/src/ordering/index.ts`; never `localeCompare`). +2. A workspace is **eligible** if at least one of its documents satisfies `P`. +3. From each eligible workspace take the single document satisfying `P` that is + least under `compareCodeUnits` on `sourcePath`, with `documentIndexInFile` + ascending as the tiebreak. +4. The accept corpus is the documents so taken from the **first 24 eligible + workspaces**. + +**`size` = 24**, one entity per workspace, from 24 distinct upstream workspaces. + +### 3.1 The size is a maintainer judgement, and is deliberately not a minimum + +ADR-0012 holds that production limits are "**not** guessed now; they must be +ratified from evidence," and ADR-0020 clause 5 declines to fix a minimum entity +count for exactly that reason. `../../spec.md` FR-055 carries the prohibition +forward: **no minimum entity count may be invented by the specification or by the +implementation.** + +24 is therefore **not** a minimum, **not** a threshold, and **not** a production +limit. It is the size of this one frozen corpus. Nothing here may be read as +ratifying a scale bound (`../../plan.md`, note on FR-055). + +The two considerations that fixed it, stated so they can be disagreed with: + +- **Upper bound — reviewability.** Every expected-path array in + `expected-paths.json` is authored by hand (T016). A corpus large enough that + the expectations must be produced by running something would defeat the + purpose: the artifact would then be code-derived, and a later "fix" to that + code would silently move the expectations. Hand authorship is only credible at + a size a reviewer can actually check line by line. +- **Lower bound — coverage with repetition.** The corpus must carry every + ownership state and every glob feature the overlay exercises (§4), each with at + least two independent instances, so no behaviour rests on a single example. + +**Whether 24 is adequate is not this document's finding to make.** ADR-0020 +clause 5 places that determination with the independent auditor, as an explicit +adequacy finding, and T019 is where it is recorded. This document states the +judgement and its reasons so that the auditor has something specific to accept or +reject. + +## 4. The overlay rule, fixed here in the same cycle + +`overlay.json` (T015) is **maintainer-authored** and is assigned by position, not +chosen per entity. Positions are 0-based over the accept corpus in the frozen +order of §3. + +| Positions | Ownership state | Overlay | +| --- | --- | --- | +| 0–21 | `explicit-paths` | annotation value = catalogue entry `C[i mod 11]` (§4.1), in the catalogue's authored input order | +| 22 | `explicit-empty` | annotation present, value exactly `"[]"` | +| 23 | `annotation-absent` | **no overlay entry at all** | + +Positional assignment is what keeps the overlay from being cherry-picked: no +entity was chosen for the annotation it would receive, because the annotation is +a function of the entity's index in an order fixed before the entities were +enumerated. + +Because `i mod 11` runs twice over positions 0–21, **each catalogue entry is +carried by exactly two distinct canonical ids.** That is deliberate: it gives +eleven independent instances of `entity-identity.md` §4's "no exclusive winner" +rule — two distinct entities whose owned paths overlap must both derive +successfully, and must not be treated as a collision. + +All three states of `owned-paths-annotation.md` §3 are present, which is what the +non-conflation rule needs: `explicit-empty` and `annotation-absent` both yield an +empty `derivedPaths`, and the corpus must be able to tell them apart. Clause 5's +"at least one non-empty annotation" floor is met 22 times over, and its warning +that an all-`annotation-absent` corpus satisfies nothing does not apply here. + +### 4.1 The pattern catalogue — hand-authored, from the frozen glob dialect + +Every pattern below is valid under `glob-dialect.md` §3's fifteen ordered rules: +POSIX segments over `A-Z a-z 0-9 _ - .` plus `*`, whole-segment `**`, and `?`; no +leading `/`, no `\`, no brace, bracket, parenthesis or comma, no leading `!`, no +`.` or `..` segment, no empty segment, and no `**` sharing a segment with other +characters. + +Input order is the authored order and is **deliberately not sorted**, because the +defect this whole cycle exists to correct is a `derivedPathPatterns` recorded in +input order rather than `compareCodeUnits` order. + +| # | Authored input order | What it exercises | +| --- | --- | --- | +| `C0` | `workspaces/alpha/src/**`, `workspaces/alpha-tools/**`, `workspaces/alpha.config/**` | `-` (0x2D) < `.` (0x2E) < `/` (0x2F) — the separator-ordering trap; input order fully reversed from sorted order | +| `C1` | `packages/core/**`, `packages/core/**`, `packages/cli/**` | duplicate element requiring dedupe | +| `C2` | `**`, `.github/**` | dotfile policy (`glob-dialect.md` §4): a bare `**` does not imply dotfile ownership, an explicit dot segment does | +| `C3` | `docs/**` | single-pattern annotation | +| `C4` | `src/utils/**`, `src/Utils/**`, `src/README.md` | case sensitivity: `R` (0x52) < `U` (0x55) < `u` (0x75) | +| `C5` | `scripts/build-?.ts`, `scripts/*.test.ts`, `scripts/build.ts` | `?` and in-segment `*`; `*` (0x2A) sorts before letters | +| `C6` | `packages/core/src/**`, `packages/core/**` | prefix nesting; partial overlap with `C1` | +| `C7` | `plugins/v2/**`, `plugins/v10/**`, `plugins/v_next/**` | digits sort lexicographically, so `v10` precedes `v2`; `_` (0x5F) after digits | +| `C8` | `.github/workflows/**`, `Makefile`, `.github/**` | uppercase `M` (0x4D) after `.` (0x2E); partial overlap with `C2` | +| `C9` | `docs/**`, `examples/**`, `docs/**` | dedupe combined with already-sorted remainder; full overlap with `C3` on `docs/**` | +| `C10` | `a/b/**`, `a-b/**`, `a/**`, `a.b/**` | all three separator characters plus `*`-before-letter, in one entry | + +## 5. How R14's known-failing populations were handled + +`../../research.md` R14 records these as **selection constraints, not incidental +facts**: "a corpus selected without regard to them would fail its own gate on +inputs that were known in advance to fail." + +Each population below was re-derived from the pinned checkout by applying +FR-016's four validator predicates and `entity-identity.md` §1's canonicalization +directly. Every figure reproduced R14 exactly; none is asserted from R14 without +having been re-derived, and none was adjusted to fit. + +| R14 fact | R14's value | Re-derived | Handling | +| --- | --- | --- | --- | +| Descriptor files (exact `catalog-info.yaml` basename) | 156 | 156 | `P`.1 | +| Entity documents | 167 | 167 | the population `P` is evaluated over | +| Files carrying any `metadata.annotations` | 23 of 156 | 23 | not a selection criterion; recorded because R14 records it | +| Descriptors carrying `adrkit.io/owned-paths` | 0 | 0 | **this is why the overlay must be maintainer-authored** — see §5.1 | +| Unsubstituted skeleton descriptors | 5 files | 5 files, all one placeholder form (`${{ values.name \| dump }}`) | excluded by `P`.4 — see §5.2 | +| Invalid `metadata.name` | 7 — **5** on character class, **2** on **length alone** | 7 — 5 character class, 2 length alone | excluded by `P`.4 | + +### 5.1 Why the overlay is maintainer-authored, and why that is not a weakness + +**Zero** descriptors in the pinned corpus carry `adrkit.io/owned-paths`. +Requiring an externally-authored annotation would make this gate contingent on +external adoption, which ADR-0014 forbids as a blocker and which ADR-0012 gate 3 +calls "welcome as an optional later production-maturity signal … **not** a hard +gate." + +ADR-0020 clause 5 adopts ADR-0012 gate 3's named construction verbatim: +"synthetic explicit annotations over pinned public corpora under independent +adversarial review" — annotations added by us, over real upstream descriptors +left otherwise untouched. That is exactly what §4 does, and the "independent +adversarial review" half is T019, not this document. + +### 5.2 The placeholder population is excluded on inadmissibility, not on collision + +R14 notes that 14 of the 16 skeleton descriptors across both corpora share +`${{ values.name | dump }}` and "collide on canonicalization". **In this corpus +they never reach canonicalization at all.** All 5 placeholder descriptors here +carry that form as `metadata.name`, which fails `isValidObjectName`, so they are +inadmissible under `P`.4 — and `../../contracts/admissibility.md` §4.1 is explicit +that an inadmissible descriptor never acquires a canonical id and therefore can +never participate in a duplicate determination in either direction. + +Recording them as excluded-for-collision would be wrong even though it reaches +the same 5 documents. `../../spec.md` FR-020 fixes the attribution: inadmissibility +is "the earlier and more specific defect." + +### 5.3 The residual valid duplicate — the Nexus pair + +`workspaces/nexus-repository-manager/plugins/nexus-repository-manager/catalog-info.yaml` +holds **two** YAML documents, both `kind: Component`, both declaring +`metadata.name: backstage-community-nexus-repository-manager`. Both are **fully +admissible** — the re-derivation above confirms all four validators return true +for both — and both canonicalize to +`component:default/backstage-community-nexus-repository-manager`. + +This is the exact pair ADR-0020's own "SC-010 cannot be satisfied under the frozen +inputs" section names, and it is the residual valid duplicate ADR-0015's Condition +of Acceptance 3 says no admissibility rule can or should reach. It is upstream's +descriptor, in a repository this project does not control. + +Both documents are excluded by `P`.5, and per §2.1 **both**, never one. Their +workspace has no other document satisfying `P`, so `nexus-repository-manager` is +not an eligible workspace under `S`.2 and contributes no entity. This is recorded +rather than passed over silently: the accept corpus is free of duplicate canonical +ids because this pair was identified and excluded, not because the corpus happened +to be clean. + +### 5.4 What no exclusion above means + +Excluding a descriptor from this accept corpus is a statement about **this +freeze**, and nothing else. It is not a claim that the descriptor is defective, +that Backstage would reject it, or that the generator may skip it at run time. +`../../spec.md` FR-018 is emphatic in the other direction: at run time an +inadmissible descriptor aborts the entire operation and is **never** skipped, +filtered, downgraded, or set aside so the remainder of a batch can succeed. + +## 6. What this freeze does not establish + +Carried from ADR-0020 clause 5 and `../../data-model.md` §17 so it is not +overstated downstream: + +- It gates **technical compatibility only**. +- It does **not** evidence that the mapping reflects anyone's actual ownership, + that anyone else wants the annotation, or that adoption risk has fallen. +- A populated, digest-verified envelope would prove **integrity, not + correctness**. Correctness is claimed only on FR-056 / SC-011's post-output + comparison at zero false positives and zero false negatives — a separate step, + in Phase F, recording its own hashes and its own PASS/FAIL, inheriting nothing + from this one. +- No claim is made about Backstage as a running system. The admissibility warrant + is exactly what the four pinned validator predicates return at commit + `1121a4facd9e321179d0402c3f355e4a649e84d9`. +- ADR-0014 **rung 1 only**. This is not reference-verified and not externally + validated, and nothing here schedules or prepares a release. + +## 7. Evidence that the rule preceded its application + +ADR-0020 clause 5's "not chosen afterwards" is an ordering requirement, and this +document cannot discharge an ordering requirement by asserting it. So the ordering +is recorded where it can be checked: + +- **§1 through §6 above — the whole rule, including `size` = 24 and the complete + pattern catalogue — were committed before the corpus was enumerated.** At that + commit, this file contained no entity list, no canonical id, and no + `sourcePath`. Nothing about which entities the rule would select was known. +- **§8 below was added by a later commit**, together with the artifacts derived + from it, and that commit did not modify §1–§6. + +Both claims are mechanically checkable and are meant to be checked rather than +believed: + +```bash +# The rule commit — §8 absent, no entity named: +git log --diff-filter=A --format=%H -- \ + specs/010-catalog-backstage/evidence/accept-corpus-freeze/selection-basis.md + +# The rule was not touched when the enumeration landed: +git diff -- \ + specs/010-catalog-backstage/evidence/accept-corpus-freeze/selection-basis.md +``` + +The second command must show additions only, and none of them above §8. Any +change to §1–§6 between those commits invalidates this freeze, and the correct +response is to redo the cycle rather than to amend the record. + +--- + +*Sections below this line were added after the rule above was committed. They are +the result of applying it, and they are recorded here so the rule and its outcome +travel together.* + +## 8. The result of applying the rule + +**Rule commit**: `654031b3ef84f3a625bc8d06d274c4ae83056b84` +**Enumeration commit**: the commit that added this section. Resolve it with +`git log --format=%H -1 -- ` after checking out the freeze, or read it +off `git log --follow -p -- `; the diff between the two commits must be +additions only, none of them above this section. + +### 8.1 The funnel, from the whole corpus to the accept corpus + +| Stage | Count | +| --- | --- | +| Entity documents in the pinned corpus | 167 | +| — failing `P`.4 (inadmissible under ADR-0015) | 7 | +| — failing `P`.5 (canonical id not unique corpus-wide) | 2 | +| Documents satisfying `P` | **158** | +| Documents satisfying `P` that lie outside `workspaces/` | 0 | +| Eligible workspaces (§3.2) | **79** | +| Workspaces taken (the first 24 in `compareCodeUnits` order) | **24** | +| **Accept corpus size** | **24** | +| Distinct canonical ids in the accept corpus | **24** | + +`nexus-repository-manager` is **not** among the 79 eligible workspaces. Its only +descriptor file holds the residual valid duplicate of §5.3, both documents of +which `P`.5 excludes, leaving that workspace with nothing to contribute. + +`azure-devops` is worth reading as a check that `P` filters before `S` orders. +That workspace holds four descriptor files. The `compareCodeUnits`-least of them +by `sourcePath` is `plugins/azure-devops-backend/catalog-info.yaml`, but its +`metadata.name` is 72 characters and fails `isValidObjectName` on length, so it +does not satisfy `P`.4 and is not a candidate. The least **candidate** — +`plugins/azure-devops-common/catalog-info.yaml` — is what `S`.3 selects. + +### 8.2 The accept corpus + +Positions are the frozen order of §3. The overlay column is the assignment fixed +in §4 before these entities were known: `C[i mod 11]` for positions 0–21, +`explicit-empty` at 22, `annotation-absent` at 23. + +| # | Workspace | Canonical id | `sourcePath` (`#documentIndexInFile`) | Overlay | +| --- | --- | --- | --- | --- | +| 0 | `acr` | `component:default/backstage-community-acr` | `workspaces/acr/plugins/acr/catalog-info.yaml` `#0` | `C0` | +| 1 | `adr` | `component:default/backstage-plugin-adr-backend` | `workspaces/adr/plugins/adr-backend/catalog-info.yaml` `#0` | `C1` | +| 2 | `agent-forge` | `component:default/backstage-plugin-agent-forge` | `workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml` `#0` | `C2` | +| 3 | `airbrake` | `component:default/backstage-plugin-airbrake-backend` | `workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml` `#0` | `C3` | +| 4 | `allure` | `component:default/backstage-plugin-allure` | `workspaces/allure/plugins/allure/catalog-info.yaml` `#0` | `C4` | +| 5 | `analytics` | `component:default/backstage-plugin-analytics-module-ga4` | `workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml` `#0` | `C5` | +| 6 | `apache-airflow` | `component:default/backstage-plugin-apache-airflow` | `workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml` `#0` | `C6` | +| 7 | `apollo-explorer` | `component:default/backstage-plugin-apollo-explorer` | `workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml` `#0` | `C7` | +| 8 | `azure-devops` | `component:default/backstage-plugin-azure-devops-common` | `workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml` `#0` | `C8` | +| 9 | `azure-sites` | `component:default/backstage-plugin-azure-sites-backend` | `workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml` `#0` | `C9` | +| 10 | `badges` | `component:default/backstage-plugin-badges-backend` | `workspaces/badges/plugins/badges-backend/catalog-info.yaml` `#0` | `C10` | +| 11 | `bazaar` | `component:default/backstage-plugin-bazaar-backend` | `workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml` `#0` | `C0` | +| 12 | `bitbucket-pull-requests` | `component:default/bitbucket-pull-requests` | `workspaces/bitbucket-pull-requests/catalog-info.yaml` `#0` | `C1` | +| 13 | `bitrise` | `component:default/backstage-plugin-bitrise` | `workspaces/bitrise/plugins/bitrise/catalog-info.yaml` `#0` | `C2` | +| 14 | `bookmarks` | `component:default/example-website` | `workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml` `#0` | `C3` | +| 15 | `catalog` | `component:default/backstage-plugin-catalog-backend-module-codeowners` | `workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml` `#0` | `C4` | +| 16 | `checkmarx` | `component:default/backstage-plugin-checkmarx-backend` | `workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml` `#0` | `C5` | +| 17 | `cicd-statistics` | `component:default/backstage-plugin-cicd-statistics-module-buildkite` | `workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml` `#0` | `C6` | +| 18 | `cloudbuild` | `component:default/backstage-plugin-cloudbuild` | `workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml` `#0` | `C7` | +| 19 | `code-climate` | `component:default/backstage-plugin-code-climate` | `workspaces/code-climate/plugins/code-climate/catalog-info.yaml` `#0` | `C8` | +| 20 | `code-coverage` | `component:default/backstage-plugin-code-coverage-backend` | `workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml` `#0` | `C9` | +| 21 | `codescene` | `component:default/backstage-plugin-codescene` | `workspaces/codescene/plugins/codescene/catalog-info.yaml` `#0` | `C10` | +| 22 | `copilot` | `component:default/backstage-plugin-copilot-backend` | `workspaces/copilot/plugins/copilot-backend/catalog-info.yaml` `#0` | `explicit-empty` | +| 23 | `cost-insights` | `component:default/backstage-plugin-cost-insights-common` | `workspaces/cost-insights/plugins/cost-insights-common/catalog-info.yaml` `#0` | `annotation-absent` | + +Two entities are worth noting because their canonical id does not follow from +their workspace name, which is exactly why the frozen record names entities by +canonical id and by `sourcePath` rather than by workspace: + +- position 14, workspace `bookmarks`, is `component:default/example-website` — + its only descriptor sits under `plugins/bookmarks/examples/component/`; +- position 12, workspace `bitbucket-pull-requests`, is + `component:default/bitbucket-pull-requests` — its descriptor is at the + workspace root rather than under `plugins/`. + +### 8.3 How the corpus facts in §5 were re-derived + +Applying `../../spec.md` FR-016's four validator predicates and +`entity-identity.md` §1's canonicalization directly to the pinned checkout, +outside this repository, over all 156 `catalog-info.yaml` files. Every figure in +§5 reproduced `research.md` R14 exactly, including the split of the seven invalid +names into **5** on character class and **2** on **length alone** — the two +populations `../../contracts/admissibility.md` §2.1 warns must never be reported +as one. + +**No ownership was derived at any point.** Nothing decoded an annotation, +compiled a glob, or matched a path. Admissibility and identity canonicalization +are `plan.md` Phase D concerns, placed **before** Barrier B, and ADR-0020 clause 5 +requires both of them in order to select an admissible, collision-free corpus at +all — a corpus could not be selected under clause 5's own conditions if computing +them were barred. + +### 8.4 Recorded hashes + +| Artifact | `contentHash` | +| --- | --- | +| `../frozen-expectations/frozen-expectation-set.json` | `e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c` | +| `accept-corpus-freeze.json` | `f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca` | + +Computed per `../README.md` §3. **T019 must recompute both from the artifacts +rather than copy them from this table** — `../../data-model.md` §16: "An audit that +transcribes the author's declared hash has verified nothing." + +### 8.5 What has *not* been done, and must not be inferred from this document + +- **No generator was run**, because none exists. `packages/adapters/catalog-backstage` + has not been created. No `SnapshotEnvelope` exists. No derived-ownership result + for any descriptor-sourced entity exists — not persisted, not held in memory, not + asserted in a test (`research.md` R4). +- **No input manifest exists anywhere in the tree**, and nothing in this evidence + tree is one (`../README.md` §4). +- **No comparison harness exists.** Phase F authors it, after this freeze and after + its audit, so that ADR-0020 clause 5's two steps stay two steps. +- **This freeze is not audited yet.** T019 is the independent audit, and it must be + performed by a reviewer with **no authoring involvement in T014–T018**. An audit + by the author of this document would not be an independent audit, and recording + one as such would defeat the control rather than implement it. Until T019 records + its own PASS/FAIL and its own **explicit adequacy finding**, SC-010 is **not** + satisfied and no part of it may be reported as satisfied. +- **T020, T021, T023's observed-failing runs have not been performed**, so under + ADR-0016 none of the checks over this freeze counts as coverage yet. +- **Barrier B has not cleared.** T024 is the gate, and it is unchecked. diff --git a/specs/010-catalog-backstage/evidence/barrier-b-checkpoint.json b/specs/010-catalog-backstage/evidence/barrier-b-checkpoint.json new file mode 100644 index 00000000..eb97d64f --- /dev/null +++ b/specs/010-catalog-backstage/evidence/barrier-b-checkpoint.json @@ -0,0 +1,58 @@ +{ + "task": "T024", + "title": "Barrier B checkpoint — hard gate", + "feature": "010-catalog-backstage", + "recordedBy": "independent auditor session (author of the T019 audit procedure and the R5 mechanism-2 drift check; NOT the author of the T014-T018 freeze)", + "timestamp": "2026-08-05T04:12:34Z", + "scopesInspected": { + "worktreeBranch": "mbeacom-t019-independent-audit", + "worktreeHead": "307669e51c03568a076a05eebec6ea954a032152", + "originMain": "99ba8d2500eaf37625ea164f66b4a17870e40dad", + "note": "origin was re-fetched at checkpoint time; Phase A merged to origin/main as 99ba8d2 (PR #84) after this worktree was branched, so origin/main was inspected in addition to this worktree, plus all local/remote branches and scratch locations (session-state files/, /tmp, .test-output)." + }, + "BARRIER_B_CLEARED": true, + "confirmations": { + "mechanism1_inputAbsence": { + "cleared": true, + "claim": "No input manifest exists anywhere in the tree, and the adapter contains no recursive walking or glob discovery that could substitute for one.", + "evidence": [ + "No input-manifest artifact and no SnapshotEnvelope instance exists under specs/010-catalog-backstage/ (git ls-files over that tree returns none).", + "The only 'input-manifest'/'snapshot-envelope' paths in the repo are spike 009 CONTRACT SPECIFICATIONS (specs/009-catalog-binding-viability/contracts/input-manifest.md and snapshot-envelope.md) — prose describing a would-be format, not an actual manifest the oracle freeze consumes. They are out of feature-010's Barrier B scope.", + "The adapter packages/adapters/catalog-backstage/ on origin/main is a Phase A placeholder: src/index.ts exports only the constant PACKAGE_NAME = '@adrkit/catalog-backstage' and has no side effect, no reader, no generator, no discovery.", + "The adapter's own guard test test/no-dynamic-loader.test.ts scans the adapter source (via test/source-scan.ts) for dynamic import()/require/createRequire/require.resolve/import.meta.resolve/Module._load and any registerAdapter/adapterRegistry/discoverAdapter-style hook, asserts the scan actually read src/index.ts by name, and drives every rule against a fixture that must trip it — so the 'no loader/discovery' conclusion is observed-firing coverage (ADR-0016), not an untested absence.", + "The consumer packages/catalog-envelope/ on origin/main is likewise a Phase A placeholder (PACKAGE_NAME only) and documents in-source that it is an integrity validator, explicitly NOT a correctness oracle." + ], + "notedDistinction": "The scanned()/readdirSync({recursive:true}) inside the adapter's test helper walks the ADAPTER'S OWN src/ and test/ to check it; that is the guard scanner inspecting the adapter, not the adapter discovering catalog inputs. It does not constitute input discovery by the adapter." + }, + "mechanism2_hashMatch": { + "cleared": true, + "claim": "The T022 drift check is green over both frozen trees.", + "evidence": [ + "scripts/check-freeze-hashes.ts re-derives the canonical content hash of every frozen artifact carrying a recorded contentHash and fails on any drift.", + "Baseline run: 'check-freeze-hashes: ok (2 artifacts: frozen-expectations/frozen-expectation-set.json, accept-corpus-freeze/accept-corpus-freeze.json)' exit 0.", + "Wired into CI as the 'Verify frozen oracle hashes match' step in .github/workflows/ci.yml (bun run check:freeze-hashes) and as the check:freeze-hashes package script.", + "The check was observed genuinely FAILING under a one-byte mutation (T023) with reason 'recorded contentHash does not match recomputed canonical hash (freeze drift)' exit 1, then restored to green — see evidence/negative-cases/freeze-drift/." + ], + "recordedHashes": { + "frozen-expectations/frozen-expectation-set.json": "e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c", + "accept-corpus-freeze/accept-corpus-freeze.json": "f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca" + } + }, + "mechanism3_ordering": { + "cleared": true, + "claim": "No comparison harness exists anywhere in the repository, in any branch of this worktree, or in any scratch location.", + "evidence": [ + "No generator-vs-oracle comparison harness exists in this worktree (tracked or untracked), on origin/main, on any local or remote branch, or in scratch locations (session-state files/, /tmp, .test-output).", + "The only path matching 'harness' repo-wide is packages/adapters/spec-kit/test/harness.ts, which is the Spec Kit extension's sandbox test scaffold (feature 003); it references neither envelope, catalog, backstage, nor compareCodeUnits and is unrelated to feature-010 oracle comparison.", + "packages/evaluator/src/compare.ts is the pre-existing generic deterministic code-unit comparator primitive (byCodeUnit, @adrkit/evaluator, referenced by ADR-0015/FR-005) — a comparator, not a harness that runs a generator and diffs output against the oracle. Its single 'envelope hash' mention is a doc comment explaining why locale-compare is banned.", + "derivedPathPatterns ordering is contract-fixed to compareCodeUnits order and was independently verified in the T019 audit; no run-time comparison harness produces or checks it." + ] + } + }, + "barrierStatement": "IS THE BARRIER. All three R5 mechanisms hold simultaneously and by inspection. No task in Phase E, F, or G may begin until this task is checked complete.", + "limitationsDisclosed": [ + "The adapter and consumer packages (packages/adapters/catalog-backstage, packages/catalog-envelope) do not exist in THIS worktree (branched before Phase A merged); they were inspected on origin/main via git show/git ls-tree at 99ba8d2. This cross-tree inspection is stated explicitly rather than implied.", + "input-manifest.md §5 (cited by T024 mechanism 1) is a spike-009 contract document, not a feature-010 artifact; the mechanism-1 conclusion rests on the absence of any input-manifest INSTANCE in feature-010's tree plus the adapter placeholder's absence of discovery, which is the substance the clause is guarding.", + "Mechanism (2)'s 'green in CI' is confirmed by the wired step running green locally; the CI run itself will execute on push. The check logic and its wiring are both present and were observed both passing and failing." + ] +} diff --git a/specs/010-catalog-backstage/evidence/frozen-expectations/README.md b/specs/010-catalog-backstage/evidence/frozen-expectations/README.md new file mode 100644 index 00000000..320fa643 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/frozen-expectations/README.md @@ -0,0 +1,34 @@ +# `frozen-expectations/` — the re-frozen oracle + +Holds the `FrozenExpectationSet` of `../../data-model.md` §16. + +| File | Written by | Present | +| --- | --- | --- | +| `frozen-expectation-set.json` | T017 | see the directory | +| `audit-record.json` | **T019** — the independent audit | not written by T014–T018 | + +## Why this directory exists at all + +ADR-0012 gate 3 is `Unmet`: the oracle exists but carries a **known-wrong** +expected result. Spike 009 froze `derivedPathPatterns` in **input order**; +`../../data-model.md` §16 and ADR-0020 clause 6 require **`compareCodeUnits`-sorted +order**, matching the `explicit-paths` requirement in +`specs/009-catalog-binding-viability/contracts/owned-paths-annotation.md` §3 that +derived paths are sorted and deduplicated. + +Correcting that ordering is the entire reason ADR-0020 clause 6 demands a *fresh* +T014 → T014a cycle rather than reuse. The spike's bundle is untracked +(ADR-0015 Condition of Acceptance 1), so nothing in the repository prevents +someone reusing the stale copy; the only control is doing the cycle again, from +scratch, and tracking the result. That is what this directory is. + +## What the audit must do, and what it may not do + +Per T019 and `../../data-model.md` §16, the auditor **recomputes** +`contentHash` from the artifact. Copying the recorded value verifies nothing. +The auditor also confirms that `derivedPathPatterns` is in `compareCodeUnits` +order and **not** in input order, and records their own PASS/FAIL. + +The auditor must have had **no authoring involvement** in T014–T018. An audit by +the author of the freeze is not an independent audit, and recording it as one +would defeat the control rather than implement it. diff --git a/specs/010-catalog-backstage/evidence/frozen-expectations/audit-record.json b/specs/010-catalog-backstage/evidence/frozen-expectations/audit-record.json new file mode 100644 index 00000000..58c099dc --- /dev/null +++ b/specs/010-catalog-backstage/evidence/frozen-expectations/audit-record.json @@ -0,0 +1,60 @@ +{ + "artifact": "audit-record", + "schemaNote": "Independent T019 auditor record for the re-frozen oracle (T017) and the derivedPathPatterns ordering. This is a sibling file to frozen-expectation-set.json and is deliberately NOT part of that artifact's canonical hash input (README.md audit-records-as-siblings rule).", + "task": "T019 (feature 010, Barrier B, ADR-0020 clause 6 oracle cycle, step T014a)", + "auditor": { + "role": "independent reviewer", + "authoringInvolvementInT014ToT018": false, + "attestation": "This session performed no authoring of T014-T018. All values recorded by the authoring session were treated as claims to be independently verified, never copied. The recorded contentHash values were never trusted as inputs; they were only compared against values this auditor recomputed from the artifact bytes with an independently written serializer.", + "baseCommitAudited": "4fff13d" + }, + "requirement1_contentHashRecomputation": { + "method": "Serializer written from scratch by this auditor (not the authoring session's), implementing evidence/README.md \u00a73 canonical form: top-level object; remove ONLY the contentHash key; keys ascending by compareCodeUnits (UTF-16 code-unit order via JS relational operators, never localeCompare); array order preserved; no insignificant whitespace; JSON string escaping; UTF-8; no trailing newline. SHA-256 over the resulting bytes. Cross-checked under both Node and Bun runtimes; byte-for-byte agreement.", + "results": [ + { + "file": "frozen-expectations/frozen-expectation-set.json", + "canonicalBytes": 9746, + "recomputed": "e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c", + "recordedInArtifact": "e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c", + "match": true + }, + { + "file": "accept-corpus-freeze/accept-corpus-freeze.json", + "canonicalBytes": 21984, + "recomputed": "f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca", + "recordedInArtifact": "f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca", + "match": true + } + ], + "verdict": "PASS", + "note": "Both recorded contentHash values are honest and the freeze is byte-stable. No astral-plane characters appear in either artifact, so UTF-16 code-unit ordering coincides with code-point ordering here; the canonical-form key sort is therefore unambiguous for this data." + }, + "requirement2_derivedPathPatternsOrdering": { + "claimUnderAudit": "derivedPathPatterns is in compareCodeUnits order, not input order (the exact defect carried forward from spike 009).", + "evidence": { + "elementCount": 25, + "isCompareCodeUnitsSorted": true, + "localeAwareSortGivesSameOrder": false, + "firstCompareCodeUnitsVsLocaleDivergence": { + "index": 0, + "compareCodeUnits": "**", + "localeCompare": ".github/**" + }, + "independentReDerivation": "Rebuilt derivedPathPatterns from first principles as the deduplicated union of every entity's expectedPaths across all 24 entities, sorted by compareCodeUnits. The re-derived sequence equals the recorded derivedPathPatterns exactly.", + "recordedFirstElement": "**", + "inputFirstAppearanceFirstElement": "workspaces/alpha-tools/**", + "differsFromInputOrder": true + }, + "comparatorSemanticsVerified": "compareCodeUnits(a,b) = a < b ? -1 : a > b ? 1 : 0 in packages/core/src/ordering/index.ts (a frozen repository primitive introduced by commit 1c21361 during Phase 6, predating feature 010). JS relational operators on strings are UTF-16 code-unit comparison, matching this comparator. A locale-aware sort demonstrably reorders the deliberately-present uppercase/lowercase (src/Utils vs src/utils) and punctuation (a/b, a-b, a., a/) cases, so the choice of comparator is load-bearing and was verified, not assumed.", + "verdict": "PASS", + "note": "The spike-009 defect (input order leaking into the frozen ordering) is not present. Ordering is contract-mandated by owned-paths-annotation.md \u00a73; the recorded order is the only admissible one under that contract." + }, + "clause6Concern_handDerivedThenChecked": { + "disclosureUnderReview": "The authoring session disclosed (selection-basis.md and accept-corpus-freeze.json disclosedArithmeticCheck) that the sort/dedup results were hand-authored and then checked against compareCodeUnits, rather than produced by running compareCodeUnits as a generator.", + "auditorJudgement": "CONSISTENT with ADR-0020 clause 6.", + "reasoning": "clause 6 bars using the generator-under-test (or its derived-ownership machinery) to manufacture the oracle. compareCodeUnits is neither: it is a frozen -1/0/1 primitive that computes no ownership and predates feature 010. R4's proscribed 'generator output' is a SnapshotEnvelope or a derived-ownership result; a two-element comparison is neither. Even under the stricter reading, the sorted+deduped ordering is fully determined by the frozen contract owned-paths-annotation.md \u00a73, so there is no degree of freedom for the author to covertly tune the oracle to match generator behaviour \u2014 the source of truth is a contract, not the generator. Disclosing this rather than burying it is to the freeze's credit.", + "residualLimitation": "The assertion 'these values were checked against, not generated by, compareCodeUnits' is a process claim that is not independently falsifiable from the artifact bytes alone. However, because the admissible ordering is uniquely fixed by contract, the backfill/gaming risk this clause guards against is nil regardless of which mechanical path produced the identical bytes." + }, + "overallVerdictThisRecord": "PASS", + "scope": "This record covers requirement 1 (both hashes) and requirement 2 (ordering) plus the clause-6 concern. The corpus ADEQUACY finding (requirement 3) and the auditor's consolidated PASS/FAIL (requirement 4) are recorded in the sibling accept-corpus-freeze/adequacy-audit.json." +} diff --git a/specs/010-catalog-backstage/evidence/frozen-expectations/frozen-expectation-set.json b/specs/010-catalog-backstage/evidence/frozen-expectations/frozen-expectation-set.json new file mode 100644 index 00000000..7e7dbbbd --- /dev/null +++ b/specs/010-catalog-backstage/evidence/frozen-expectations/frozen-expectation-set.json @@ -0,0 +1,275 @@ +{ + "//": "T017 — the re-frozen oracle. FrozenExpectationSet, data-model.md §16. This is the fresh T014 step of ADR-0020 clause 6's T014 → T014a cycle. The T014a step (the independent audit) is T019 and is recorded in the sibling file audit-record.json, which does not exist yet and must not be written by the author of this file.", + "task": "T017", + "barrierSide": "IS THE BARRIER", + "discharges": [ + "FR-053" + ], + "frozenAt": "2026-08-05T03:43:58Z", + "correction": { + "whatWasWrong": "Spike 009 froze `derivedPathPatterns` in INPUT order. ADR-0012 gate 3 records the consequence: 'The oracle exists but carries a known-wrong expected result and must be re-frozen.'", + "whatIsRightAndWhyItIsRight": "`derivedPathPatterns` below is in `compareCodeUnits`-sorted order. data-model.md §16 names this correction directly, and it follows from owned-paths-annotation.md §3, which requires an `explicit-paths` derivation to be sorted and deduplicated. An oracle recording input order is asserting an expectation the derivation contract forbids.", + "whyReuseWasNotAnOption": "ADR-0020 clause 6 requires a FRESH cycle — correct the ordering, re-freeze, re-hash, obtain a NEW independent pre-output audit — rather than an amendment to the spike's artifact. ADR-0015's Condition of Acceptance 1 records why: the spike's evidence bundle is untracked and scratch-only, so 'nothing in the repository will stop someone reusing a stale copy.' Nothing in this file is copied from, or reconciled against, that bundle.", + "howAnAuditorFalsifiesTheOrdering": "Sort `derivedPathPatterns` with `compareCodeUnits` (`a < b ? -1 : a > b ? 1 : 0`, packages/core/src/ordering/index.ts) and compare element-by-element with the array as recorded. They must be identical. Then confirm the array is NOT merely the concatenation of the overlay's annotation values in authored order — for this corpus the two disagree at the first element (`**` sorted, `workspaces/alpha/src/**` in input order), so the distinction is visible immediately rather than only in the tail." + }, + "derivedPathPatterns": [ + "**", + ".github/**", + ".github/workflows/**", + "Makefile", + "a-b/**", + "a.b/**", + "a/**", + "a/b/**", + "docs/**", + "examples/**", + "packages/cli/**", + "packages/core/**", + "packages/core/src/**", + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**", + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts", + "src/README.md", + "src/Utils/**", + "src/utils/**", + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ], + "derivedPathPatternsProvenance": { + "definition": "The deduplicated union of every pattern appearing in any entity's `expectedPaths`, in ascending `compareCodeUnits` order.", + "count": 25, + "sourceOfTruth": "accept-corpus-freeze/expected-paths.json, itself hand-derived from accept-corpus-freeze/overlay.json under the frozen contracts. Not produced by any generator; none exists.", + "orderingTrapsItEncodes": [ + "`*` (0x2A) precedes `.` (0x2E), so a bare `**` sorts before `.github/**`.", + "`M` (0x4D) precedes every lowercase letter, so `Makefile` sorts before `a-b/**`.", + "`-` (0x2D) < `.` (0x2E) < `/` (0x2F), so `a-b/**` and `a.b/**` both precede `a/**`.", + "`*` precedes letters within a shared prefix, so `a/**` precedes `a/b/**` and `packages/core/**` precedes `packages/core/src/**`.", + "digits sort lexically, so `plugins/v10/**` precedes `plugins/v2/**`; `_` (0x5F) follows digits, so `plugins/v_next/**` is last of the three.", + "uppercase precedes lowercase, so `src/README.md` < `src/Utils/**` < `src/utils/**`." + ] + }, + "expectedByEntity": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "expectedPaths": [] + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "expectedPaths": [] + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**" + ] + } + ], + "expectedByEntityNotes": { + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "count": 24, + "whyOwnershipStateIsPresent": "data-model.md §16 sketches `expectedByEntity` as `{ canonicalId, expectedPaths }`. `ownershipState` is carried in addition because owned-paths-annotation.md §3's non-conflation rule requires the discriminator to be an explicit field: `explicit-empty` and `annotation-absent` both yield an empty array, and the rule forbids inferring the distinction from `derivedPaths` alone. An oracle recording only the two sketched fields could not express a distinction the derivation contract mandates. The addition is a superset of §16's shape and removes nothing from it.", + "whyAuditRecordIsNotHere": "data-model.md §16 gives FrozenExpectationSet an `auditRecord` member. It is deliberately absent from this file. Writing the audit into the artifact it audits would change that artifact's bytes and therefore its hash, so the recorded hash could never match a re-derivation. The audit is written by T019 to the sibling file `audit-record.json`, which is also what T019's own task line specifies. The composed §16 type is the pair taken together; `contentHash` covers the frozen half." + }, + "contentHash": "e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c", + "contentHashNotes": { + "algorithm": "Lowercase-hex SHA-256 over the canonical form defined in ../README.md §3: this top-level object with the `contentHash` key removed and nothing else removed, serialized with object keys ordered ascending by `compareCodeUnits`, array order preserved, no insignificant whitespace, UTF-8, no trailing newline.", + "forTheAuditor": "Recompute it. Do not copy it. data-model.md §16 states the rule for a reason: 'An audit that transcribes the author's declared hash has verified nothing.'" + }, + "warrantAndLimits": { + "whatThisArtifactIs": "A statement of what the derivation is EXPECTED to produce, fixed before any generator exists.", + "whatItIsNot": [ + "It is not evidence that any generator produces these values. No generator has been written or run.", + "It is not correctness evidence. Correctness is claimed only on FR-056 / SC-011's post-output comparison at zero false positives and zero false negatives, which is a separate step in Phase F recording its own hashes and its own PASS/FAIL and inheriting nothing from this one.", + "It is not a claim about Backstage as a running system.", + "It is ADR-0014 rung 1 only — not reference-verified, not externally validated. Only the corpus data is third-party; this oracle is the maintainer's own." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/README.md b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/README.md new file mode 100644 index 00000000..2329fb86 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/README.md @@ -0,0 +1,22 @@ +# Negative case: integrity-only audit (T021) + +**Retained permanent negative case for the ADR-0016 observation of SC-010.** + +The two frozen artifacts here are correct (hashes match), but the +`adequacy-audit.json` **confirms integrity and stops** — it records no adequacy +finding. Per ADR-0020 clause 5(a) / SC-010, an integrity confirmation alone does +not satisfy the gate and MUST be recorded as FAIL, never silently accepted. This +is the exact failure mode the T019 adequacy requirement exists to prevent. + +Observed reason string (verbatim), from +`bun scripts/audit-oracle-freeze.ts `: + +``` +FAIL [adequacy]: audit confirmed integrity but recorded no adequacy finding — SC-010 requires an explicit adequacy determination, an integrity confirmation alone does not satisfy clause 5(a) +exit=1 +``` + +Restoring an explicit adequacy finding (the live evidence tree, whose +`adequacy-audit.json` records finding = ADEQUATE) returns the audit to PASS +(exit 0). Do not add an adequacy finding to this variant — it is retained broken +on purpose. diff --git a/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/accept-corpus-freeze/accept-corpus-freeze.json b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/accept-corpus-freeze/accept-corpus-freeze.json new file mode 100644 index 00000000..6e7e9058 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/accept-corpus-freeze/accept-corpus-freeze.json @@ -0,0 +1,447 @@ +{ + "//": "T018 — AcceptCorpusFreeze, data-model.md §17. The ADR-0020 clause-5 gate artifact, assembled in the SAME CYCLE as T014 (selection basis and size), T015 (overlay), T016 (expected paths) and T017 (the re-frozen oracle). This artifact and the T017 oracle are frozen together or not at all.", + "task": "T018", + "barrierSide": "IS THE BARRIER", + "discharges": [ + "FR-054 (same-cycle freeze)" + ], + "frozenAt": "2026-08-05T03:43:58Z", + "corpusRef": { + "repository": "github.com/backstage/community-plugins", + "commit": "92e9e4e09c76cc57f3475029b73e5ec84498a459" + }, + "selectionBasis": "Recorded in full at accept-corpus-freeze/selection-basis.md, and committed BEFORE it was applied (that file, §7). In summary: over all 167 entity documents of the pinned corpus, a document satisfies P when its file basename is exactly `catalog-info.yaml`; it parses as YAML with uniqueKeys and zero parse errors; it is entity-shaped; it is admissible under ADR-0015's four field validators as reproduced at spec.md FR-016 and pinned to Backstage commit 1121a4facd9e321179d0402c3f355e4a649e84d9; and its canonical id (entity-identity.md §1) is unique across all admissible documents of the corpus, with EVERY member of any colliding group excluded rather than one member kept — keeping one would be last-wins resolution, which entity-identity.md §3 forbids, moved earlier so it is harder to see. The corpus is then one entity per workspace: workspaces ordered ascending by compareCodeUnits, a workspace eligible when at least one of its documents satisfies P, and from each eligible workspace the compareCodeUnits-least satisfying document by sourcePath with documentIndexInFile ascending as tiebreak — taken from the first 24 eligible workspaces. 158 of 167 documents satisfy P; 79 workspaces are eligible; 24 are selected.", + "size": 24, + "sizeIsNotAMinimum": "ADR-0012 holds that production limits are not guessed now but ratified from evidence, and ADR-0020 clause 5 declines to fix a minimum entity count for that reason; spec.md FR-055 forbids the specification or the implementation inventing one. 24 is the size of THIS frozen corpus. It is not a minimum, not a threshold, and not a production limit, and nothing here ratifies a scale bound. Whether 24 is adequate is the independent auditor's explicit finding to make at T019, not this artifact's to assert.", + "corpusFacts": { + "note": "Each figure was re-derived from the pin by applying FR-016's four validator predicates and entity-identity.md §1 directly, and each reproduced research.md R14 exactly. None is asserted from R14 without re-derivation, and none was adjusted to fit.", + "descriptorFilesExactBasename": 156, + "entityDocuments": 167, + "filesCarryingAnyAnnotations": 23, + "documentsCarryingOwnedPathsAnnotationUpstream": 0, + "unsubstitutedSkeletonFiles": 5, + "documentsWithInvalidMetadataName": { + "total": 7, + "failingOnCharacterClass": 5, + "failingOnLengthAlone": 2 + }, + "twoPopulationsThatAreNotOnePopulation": "\"Over 63 characters\" and \"invalid\" are different sets (research.md R14; contracts/admissibility.md §2.1). Of the 7, five fail on character class and two fail on length ALONE. Any figure that collapses them is wrong even when its total is right.", + "documentsFailingP4Inadmissible": 7, + "documentsFailingP5CollidingCanonicalId": 2, + "documentsSatisfyingP": 158, + "eligibleWorkspaces": 79, + "excludedPlaceholderPopulation": "All 5 unsubstituted skeleton descriptors carry `${{ values.name | dump }}` as metadata.name, which fails isValidObjectName. They are excluded on INADMISSIBILITY, not on collision: contracts/admissibility.md §4.1 is explicit that an inadmissible descriptor never acquires a canonical id and so can never participate in a duplicate determination in either direction, and spec.md FR-020 fixes inadmissibility as the earlier and more specific defect.", + "excludedResidualValidDuplicate": "workspaces/nexus-repository-manager/plugins/nexus-repository-manager/catalog-info.yaml holds two documents, both fully admissible, both canonicalizing to component:default/backstage-community-nexus-repository-manager. This is the pair ADR-0020 names as the reason spike 009's SC-010 was unsatisfiable under the frozen inputs, and the residual valid duplicate ADR-0015 Condition of Acceptance 3 says no admissibility rule can or should reach. BOTH documents are excluded, never one. That workspace has no other qualifying document and is therefore not eligible, contributing no entity. The corpus is free of duplicate canonical ids because this pair was identified and excluded, not because the corpus happened to be clean.", + "whatExclusionDoesNotMean": "Excluding a descriptor from this accept corpus is a statement about this freeze and nothing else. It is not a claim the descriptor is defective, nor licence for the generator to skip anything at run time — spec.md FR-018 requires an inadmissible descriptor to abort the entire operation, never to be skipped, filtered, downgraded, or set aside so the remainder of a batch can succeed." + }, + "sameCycleAttestation": { + "requirement": "ADR-0020 clause 5 requires the corpus, its overlay, its expected paths, and its selection basis and size to be frozen within the same T014 → T014a cycle, before any generator output; SC-010 and tasks.md T018 repeat that this artifact and the T017 oracle are frozen together or not at all.", + "howItWasSatisfied": "T014 through T018 were authored in one uninterrupted session and landed across exactly two commits. The first commit contains ONLY the rule (selection-basis.md §1–§6, naming no entity, no canonical id and no sourcePath) plus the T013 evidence tree. The second commit — the commit that adds this file — contains the enumeration and every artifact derived from the rule, and does not modify §1–§6.", + "ruleCommit": "654031b3ef84f3a625bc8d06d274c4ae83056b84", + "freezeCommit": "The commit that adds this file. Resolve it with: git log --diff-filter=A --format=%H -- specs/010-catalog-backstage/evidence/accept-corpus-freeze/accept-corpus-freeze.json", + "whatTheAuditorShouldCheck": "That `git diff -- .../selection-basis.md` shows additions only, none of them above §8. Any change to §1–§6 between those commits invalidates this freeze, and the correct response is to redo the cycle rather than amend the record.", + "noGeneratorExisted": "At neither commit does packages/adapters/catalog-backstage exist, and no generator has been written, built, or run. No SnapshotEnvelope exists. No derived-ownership result for any descriptor-sourced entity exists — not persisted, not in memory, not asserted in a test (research.md R4). No input manifest exists anywhere in the tree, and no comparison harness exists anywhere in the repository." + }, + "overlay": [ + { + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[]" + } + ], + "overlayProvenance": { + "authorship": "maintainer-authored", + "entries": 23, + "ordering": "Ascending `compareCodeUnits` on `sourcePath`, with `documentIndexInFile` ascending as tiebreak. For this corpus that order coincides with the frozen selection order of selection-basis.md §3, because each of the 24 selected entities comes from a distinct workspace and no selected workspace name is a strict prefix of another.", + "statedPlainly": "Zero descriptors in the pinned corpus carry adrkit.io/owned-paths — R14 records it and this freeze re-derived it. Every annotation value is the maintainer's own, written over real upstream descriptors left otherwise unmodified. ADR-0020 clause 5 adopts ADR-0012 gate 3's named construction, \"synthetic explicit annotations over pinned public corpora under independent adversarial review\"; the independent adversarial review half is T019. Only the corpus data is third-party — never the validation (ADR-0014 honesty rules).", + "ownershipStatesExercised": { + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1 + }, + "clause5Floor": "Clause 5 requires at least one non-empty annotation on a real entity; this overlay carries 22, so the all-annotation-absent corpus clause 5 warns satisfies nothing is not what was frozen." + }, + "expectedPaths": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation IS present and IS a string scalar; it decodes via JSON.parse to an array of length zero. owned-paths-annotation.md §3 requires this to be a decoded-value check, never a raw-string equality check, and §4 requires it not to be confused with a single empty-string element, which would instead be rejected at the glob dialect's `empty` rule." + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "sourcePath": "workspaces/cost-insights/plugins/cost-insights-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation key is wholly absent — there is no overlay entry for this entity. owned-paths-annotation.md §1 step 1 decides presence by an explicit discriminant, never by inferring it from a raw value being undefined, and §3's non-conflation rule forbids treating this as equivalent to the explicit-empty entity above." + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**" + ] + } + ], + "expectedPathsProvenance": { + "entries": 24, + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "howProduced": "By hand, by applying the frozen contracts to the maintainer-authored overlay values in overlay.json. Nothing here was produced by, checked against, or adjusted to match any generator. No generator exists at this point: no package has been created, and Phase E may not begin until T024.", + "disclosedArithmeticCheck": "Each `expectedPaths` array below was written by hand and then checked against `compareCodeUnits` — whose entire definition is `a < b ? -1 : a > b ? 1 : 0` over UTF-16 code units (packages/core/src/ordering/index.ts) — to confirm the hand ordering was not miscounted. This is disclosed rather than done silently. It is not generator output under research.md R4: a two-line comparator promoted into `@adrkit/core` is a frozen repository primitive, not the assembled generator, and it computes no ownership result. All eleven arrays and the 25-element union matched the hand-written values on the first check.", + "agreementWithTheOracle": "Every (canonicalId, expectedPaths) pair here is identical to the corresponding entry in frozen-expectations/frozen-expectation-set.json. The two artifacts are two views of one freeze, and a divergence between them is a freeze failure, not a discrepancy to be reconciled." + }, + "contentHash": "f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca", + "contentHashNotes": { + "algorithm": "Lowercase-hex SHA-256 over the canonical form defined in ../README.md §3: this top-level object with the `contentHash` key removed and nothing else removed, keys ordered ascending by compareCodeUnits, array order preserved, no insignificant whitespace, UTF-8, no trailing newline.", + "forTheAuditor": "Recompute it from this artifact. Do not copy it. data-model.md §16: \"An audit that transcribes the author's declared hash has verified nothing.\"" + }, + "warrantAndLimits": { + "whatThisFreezeBuys": "It fixes, in advance and in the open, what ownership derivation over this corpus is expected to produce. Per ADR-0020 clause 5 it exercises derivation against real descriptor structure and real field shapes, and it gates TECHNICAL COMPATIBILITY ONLY.", + "whatItDoesNotBuy": [ + "It does not evidence that the mapping reflects anyone's actual ownership.", + "It does not evidence that anyone else wants the annotation, and adoption remains entirely ungated.", + "It does not evidence that adoption risk has fallen.", + "It is not correctness evidence. A populated, digest-verified envelope would prove INTEGRITY, NOT CORRECTNESS — a semantically wrong envelope can carry a perfectly valid self-digest (ADR-0020 clause 5; spec.md FR-058; SC-012). Correctness is claimed only on FR-056 / SC-011, in Phase F, recording its own hashes and its own PASS/FAIL and inheriting nothing from this step.", + "It says nothing about Backstage as a running system. The admissibility warrant is exactly what the four pinned validator predicates return at Backstage commit 1121a4facd9e321179d0402c3f355e4a649e84d9.", + "ADR-0014 rung 1 only — not reference-verified, not externally validated, and no release is scheduled, prepared, or implied." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/accept-corpus-freeze/adequacy-audit.json b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/accept-corpus-freeze/adequacy-audit.json new file mode 100644 index 00000000..fdb93a7e --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/accept-corpus-freeze/adequacy-audit.json @@ -0,0 +1,11 @@ +{ + "artifact": "adequacy-audit", + "note": "DELIBERATE NEGATIVE CASE (T021). This audit confirmed hash integrity but never reached an adequacy determination. Per ADR-0020 clause 5(a)/SC-010 this MUST be recorded as FAIL, not silently accepted.", + "requirement1_contentHashRecomputation": { + "verdict": "PASS", + "note": "hashes match" + }, + "requirement3_adequacyFinding": { + "note": "intentionally omitted — no finding field present" + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/frozen-expectations/frozen-expectation-set.json b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/frozen-expectations/frozen-expectation-set.json new file mode 100644 index 00000000..7e7dbbbd --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/audit-integrity-only/frozen-expectations/frozen-expectation-set.json @@ -0,0 +1,275 @@ +{ + "//": "T017 — the re-frozen oracle. FrozenExpectationSet, data-model.md §16. This is the fresh T014 step of ADR-0020 clause 6's T014 → T014a cycle. The T014a step (the independent audit) is T019 and is recorded in the sibling file audit-record.json, which does not exist yet and must not be written by the author of this file.", + "task": "T017", + "barrierSide": "IS THE BARRIER", + "discharges": [ + "FR-053" + ], + "frozenAt": "2026-08-05T03:43:58Z", + "correction": { + "whatWasWrong": "Spike 009 froze `derivedPathPatterns` in INPUT order. ADR-0012 gate 3 records the consequence: 'The oracle exists but carries a known-wrong expected result and must be re-frozen.'", + "whatIsRightAndWhyItIsRight": "`derivedPathPatterns` below is in `compareCodeUnits`-sorted order. data-model.md §16 names this correction directly, and it follows from owned-paths-annotation.md §3, which requires an `explicit-paths` derivation to be sorted and deduplicated. An oracle recording input order is asserting an expectation the derivation contract forbids.", + "whyReuseWasNotAnOption": "ADR-0020 clause 6 requires a FRESH cycle — correct the ordering, re-freeze, re-hash, obtain a NEW independent pre-output audit — rather than an amendment to the spike's artifact. ADR-0015's Condition of Acceptance 1 records why: the spike's evidence bundle is untracked and scratch-only, so 'nothing in the repository will stop someone reusing a stale copy.' Nothing in this file is copied from, or reconciled against, that bundle.", + "howAnAuditorFalsifiesTheOrdering": "Sort `derivedPathPatterns` with `compareCodeUnits` (`a < b ? -1 : a > b ? 1 : 0`, packages/core/src/ordering/index.ts) and compare element-by-element with the array as recorded. They must be identical. Then confirm the array is NOT merely the concatenation of the overlay's annotation values in authored order — for this corpus the two disagree at the first element (`**` sorted, `workspaces/alpha/src/**` in input order), so the distinction is visible immediately rather than only in the tail." + }, + "derivedPathPatterns": [ + "**", + ".github/**", + ".github/workflows/**", + "Makefile", + "a-b/**", + "a.b/**", + "a/**", + "a/b/**", + "docs/**", + "examples/**", + "packages/cli/**", + "packages/core/**", + "packages/core/src/**", + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**", + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts", + "src/README.md", + "src/Utils/**", + "src/utils/**", + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ], + "derivedPathPatternsProvenance": { + "definition": "The deduplicated union of every pattern appearing in any entity's `expectedPaths`, in ascending `compareCodeUnits` order.", + "count": 25, + "sourceOfTruth": "accept-corpus-freeze/expected-paths.json, itself hand-derived from accept-corpus-freeze/overlay.json under the frozen contracts. Not produced by any generator; none exists.", + "orderingTrapsItEncodes": [ + "`*` (0x2A) precedes `.` (0x2E), so a bare `**` sorts before `.github/**`.", + "`M` (0x4D) precedes every lowercase letter, so `Makefile` sorts before `a-b/**`.", + "`-` (0x2D) < `.` (0x2E) < `/` (0x2F), so `a-b/**` and `a.b/**` both precede `a/**`.", + "`*` precedes letters within a shared prefix, so `a/**` precedes `a/b/**` and `packages/core/**` precedes `packages/core/src/**`.", + "digits sort lexically, so `plugins/v10/**` precedes `plugins/v2/**`; `_` (0x5F) follows digits, so `plugins/v_next/**` is last of the three.", + "uppercase precedes lowercase, so `src/README.md` < `src/Utils/**` < `src/utils/**`." + ] + }, + "expectedByEntity": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "expectedPaths": [] + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "expectedPaths": [] + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**" + ] + } + ], + "expectedByEntityNotes": { + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "count": 24, + "whyOwnershipStateIsPresent": "data-model.md §16 sketches `expectedByEntity` as `{ canonicalId, expectedPaths }`. `ownershipState` is carried in addition because owned-paths-annotation.md §3's non-conflation rule requires the discriminator to be an explicit field: `explicit-empty` and `annotation-absent` both yield an empty array, and the rule forbids inferring the distinction from `derivedPaths` alone. An oracle recording only the two sketched fields could not express a distinction the derivation contract mandates. The addition is a superset of §16's shape and removes nothing from it.", + "whyAuditRecordIsNotHere": "data-model.md §16 gives FrozenExpectationSet an `auditRecord` member. It is deliberately absent from this file. Writing the audit into the artifact it audits would change that artifact's bytes and therefore its hash, so the recorded hash could never match a re-derivation. The audit is written by T019 to the sibling file `audit-record.json`, which is also what T019's own task line specifies. The composed §16 type is the pair taken together; `contentHash` covers the frozen half." + }, + "contentHash": "e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c", + "contentHashNotes": { + "algorithm": "Lowercase-hex SHA-256 over the canonical form defined in ../README.md §3: this top-level object with the `contentHash` key removed and nothing else removed, serialized with object keys ordered ascending by `compareCodeUnits`, array order preserved, no insignificant whitespace, UTF-8, no trailing newline.", + "forTheAuditor": "Recompute it. Do not copy it. data-model.md §16 states the rule for a reason: 'An audit that transcribes the author's declared hash has verified nothing.'" + }, + "warrantAndLimits": { + "whatThisArtifactIs": "A statement of what the derivation is EXPECTED to produce, fixed before any generator exists.", + "whatItIsNot": [ + "It is not evidence that any generator produces these values. No generator has been written or run.", + "It is not correctness evidence. Correctness is claimed only on FR-056 / SC-011's post-output comparison at zero false positives and zero false negatives, which is a separate step in Phase F recording its own hashes and its own PASS/FAIL and inheriting nothing from this one.", + "It is not a claim about Backstage as a running system.", + "It is ADR-0014 rung 1 only — not reference-verified, not externally validated. Only the corpus data is third-party; this oracle is the maintainer's own." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/freeze-drift/README.md b/specs/010-catalog-backstage/evidence/negative-cases/freeze-drift/README.md new file mode 100644 index 00000000..424afcbb --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/freeze-drift/README.md @@ -0,0 +1,25 @@ +# Negative case: freeze-hash drift (T023) + +**Retained permanent negative case for the ADR-0016 observation of the R5 +mechanism-2 drift check (`scripts/check-freeze-hashes.ts`).** + +The drift check re-derives the canonical content hash of every frozen artifact +and fails CI on any divergence from the recorded `contentHash`. To observe it +genuinely failing, a single byte of `frozen-expectation-set.json` was flipped +inside a value (`workspaces/alpha/src/**` → `workspaces/blpha/src/**`, one +character, JSON still parseable) in an **isolated copy** — the live frozen +artifact was never left mutated. + +Observed reason string (verbatim), from +`bun scripts/check-freeze-hashes.ts `: + +``` +frozen-expectations/frozen-expectation-set.json: recorded contentHash does not match recomputed canonical hash (freeze drift) recorded=e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c recomputed=311e54427cce1b1564adb1e4467c7c57b868b61e2a8cc13e2483007cc4e1413f +exit=1 +``` + +Restoring the byte returns the check to `ok` (exit 0). The observation is also +locked in the suite by `scripts/check-freeze-hashes.test.ts` (T023), which +performs the mutate → FAIL → restore → PASS cycle against a copied evidence +tree. This directory records the exact observed output; it holds no mutated +artifact of its own so that the drift check never fails on the live corpus. diff --git a/specs/010-catalog-backstage/evidence/negative-cases/freeze-drift/observed-fail.json b/specs/010-catalog-backstage/evidence/negative-cases/freeze-drift/observed-fail.json new file mode 100644 index 00000000..34a50e82 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/freeze-drift/observed-fail.json @@ -0,0 +1,22 @@ +{ + "task": "T023", + "observationOf": "scripts/check-freeze-hashes.ts (R5 mechanism 2 — CI freeze-hash drift check)", + "adr": "ADR-0016 (a check must be observed failing before it counts as coverage)", + "mutation": { + "artifact": "frozen-expectations/frozen-expectation-set.json", + "kind": "single-byte value flip", + "change": "workspaces/alpha/src/** -> workspaces/blpha/src/**", + "bytesChanged": 1, + "jsonStillParseable": true, + "performedOn": "an isolated copy; the live frozen artifact was never left mutated" + }, + "observedFail": { + "stderr": "frozen-expectations/frozen-expectation-set.json: recorded contentHash does not match recomputed canonical hash (freeze drift) recorded=e641ae5e4201a099e92e98fbaa7683bc0eb0290adb01f93abbd474c695c2430c recomputed=311e54427cce1b1564adb1e4467c7c57b868b61e2a8cc13e2483007cc4e1413f", + "exitCode": 1 + }, + "restore": { + "action": "restore the flipped byte", + "observedResult": "check-freeze-hashes: ok", + "exitCode": 0 + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/README.md b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/README.md new file mode 100644 index 00000000..2b94a0ca --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/README.md @@ -0,0 +1,22 @@ +# Negative case: oracle in input order (T020) + +**Retained permanent negative case for the ADR-0016 observation of FR-053.** + +This is a deliberately-broken copy of the frozen oracle whose +`derivedPathPatterns` is in **input order** (first-appearance across entities, +in file order) rather than `compareCodeUnits` order — the exact defect carried +forward from spike 009. Its `contentHash` is recomputed to be internally valid, +so the integrity check passes and **ordering is the sole failure**, proving the +audit catches the defect independent of hash integrity. + +Observed reason string (verbatim), from +`bun scripts/audit-oracle-freeze.ts `: + +``` +FAIL [ordering]: derivedPathPatterns is not in compareCodeUnits order (input order or any other order is inadmissible) +exit=1 +``` + +Restoring the correct compareCodeUnits-ordered artifact (the live evidence tree) +returns the audit to PASS (exit 0). Do not "fix" this variant — it is retained +broken on purpose. diff --git a/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/accept-corpus-freeze/accept-corpus-freeze.json b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/accept-corpus-freeze/accept-corpus-freeze.json new file mode 100644 index 00000000..6e7e9058 --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/accept-corpus-freeze/accept-corpus-freeze.json @@ -0,0 +1,447 @@ +{ + "//": "T018 — AcceptCorpusFreeze, data-model.md §17. The ADR-0020 clause-5 gate artifact, assembled in the SAME CYCLE as T014 (selection basis and size), T015 (overlay), T016 (expected paths) and T017 (the re-frozen oracle). This artifact and the T017 oracle are frozen together or not at all.", + "task": "T018", + "barrierSide": "IS THE BARRIER", + "discharges": [ + "FR-054 (same-cycle freeze)" + ], + "frozenAt": "2026-08-05T03:43:58Z", + "corpusRef": { + "repository": "github.com/backstage/community-plugins", + "commit": "92e9e4e09c76cc57f3475029b73e5ec84498a459" + }, + "selectionBasis": "Recorded in full at accept-corpus-freeze/selection-basis.md, and committed BEFORE it was applied (that file, §7). In summary: over all 167 entity documents of the pinned corpus, a document satisfies P when its file basename is exactly `catalog-info.yaml`; it parses as YAML with uniqueKeys and zero parse errors; it is entity-shaped; it is admissible under ADR-0015's four field validators as reproduced at spec.md FR-016 and pinned to Backstage commit 1121a4facd9e321179d0402c3f355e4a649e84d9; and its canonical id (entity-identity.md §1) is unique across all admissible documents of the corpus, with EVERY member of any colliding group excluded rather than one member kept — keeping one would be last-wins resolution, which entity-identity.md §3 forbids, moved earlier so it is harder to see. The corpus is then one entity per workspace: workspaces ordered ascending by compareCodeUnits, a workspace eligible when at least one of its documents satisfies P, and from each eligible workspace the compareCodeUnits-least satisfying document by sourcePath with documentIndexInFile ascending as tiebreak — taken from the first 24 eligible workspaces. 158 of 167 documents satisfy P; 79 workspaces are eligible; 24 are selected.", + "size": 24, + "sizeIsNotAMinimum": "ADR-0012 holds that production limits are not guessed now but ratified from evidence, and ADR-0020 clause 5 declines to fix a minimum entity count for that reason; spec.md FR-055 forbids the specification or the implementation inventing one. 24 is the size of THIS frozen corpus. It is not a minimum, not a threshold, and not a production limit, and nothing here ratifies a scale bound. Whether 24 is adequate is the independent auditor's explicit finding to make at T019, not this artifact's to assert.", + "corpusFacts": { + "note": "Each figure was re-derived from the pin by applying FR-016's four validator predicates and entity-identity.md §1 directly, and each reproduced research.md R14 exactly. None is asserted from R14 without re-derivation, and none was adjusted to fit.", + "descriptorFilesExactBasename": 156, + "entityDocuments": 167, + "filesCarryingAnyAnnotations": 23, + "documentsCarryingOwnedPathsAnnotationUpstream": 0, + "unsubstitutedSkeletonFiles": 5, + "documentsWithInvalidMetadataName": { + "total": 7, + "failingOnCharacterClass": 5, + "failingOnLengthAlone": 2 + }, + "twoPopulationsThatAreNotOnePopulation": "\"Over 63 characters\" and \"invalid\" are different sets (research.md R14; contracts/admissibility.md §2.1). Of the 7, five fail on character class and two fail on length ALONE. Any figure that collapses them is wrong even when its total is right.", + "documentsFailingP4Inadmissible": 7, + "documentsFailingP5CollidingCanonicalId": 2, + "documentsSatisfyingP": 158, + "eligibleWorkspaces": 79, + "excludedPlaceholderPopulation": "All 5 unsubstituted skeleton descriptors carry `${{ values.name | dump }}` as metadata.name, which fails isValidObjectName. They are excluded on INADMISSIBILITY, not on collision: contracts/admissibility.md §4.1 is explicit that an inadmissible descriptor never acquires a canonical id and so can never participate in a duplicate determination in either direction, and spec.md FR-020 fixes inadmissibility as the earlier and more specific defect.", + "excludedResidualValidDuplicate": "workspaces/nexus-repository-manager/plugins/nexus-repository-manager/catalog-info.yaml holds two documents, both fully admissible, both canonicalizing to component:default/backstage-community-nexus-repository-manager. This is the pair ADR-0020 names as the reason spike 009's SC-010 was unsatisfiable under the frozen inputs, and the residual valid duplicate ADR-0015 Condition of Acceptance 3 says no admissibility rule can or should reach. BOTH documents are excluded, never one. That workspace has no other qualifying document and is therefore not eligible, contributing no entity. The corpus is free of duplicate canonical ids because this pair was identified and excluded, not because the corpus happened to be clean.", + "whatExclusionDoesNotMean": "Excluding a descriptor from this accept corpus is a statement about this freeze and nothing else. It is not a claim the descriptor is defective, nor licence for the generator to skip anything at run time — spec.md FR-018 requires an inadmissible descriptor to abort the entire operation, never to be skipped, filtered, downgraded, or set aside so the remainder of a batch can succeed." + }, + "sameCycleAttestation": { + "requirement": "ADR-0020 clause 5 requires the corpus, its overlay, its expected paths, and its selection basis and size to be frozen within the same T014 → T014a cycle, before any generator output; SC-010 and tasks.md T018 repeat that this artifact and the T017 oracle are frozen together or not at all.", + "howItWasSatisfied": "T014 through T018 were authored in one uninterrupted session and landed across exactly two commits. The first commit contains ONLY the rule (selection-basis.md §1–§6, naming no entity, no canonical id and no sourcePath) plus the T013 evidence tree. The second commit — the commit that adds this file — contains the enumeration and every artifact derived from the rule, and does not modify §1–§6.", + "ruleCommit": "654031b3ef84f3a625bc8d06d274c4ae83056b84", + "freezeCommit": "The commit that adds this file. Resolve it with: git log --diff-filter=A --format=%H -- specs/010-catalog-backstage/evidence/accept-corpus-freeze/accept-corpus-freeze.json", + "whatTheAuditorShouldCheck": "That `git diff -- .../selection-basis.md` shows additions only, none of them above §8. Any change to §1–§6 between those commits invalidates this freeze, and the correct response is to redo the cycle rather than amend the record.", + "noGeneratorExisted": "At neither commit does packages/adapters/catalog-backstage exist, and no generator has been written, built, or run. No SnapshotEnvelope exists. No derived-ownership result for any descriptor-sourced entity exists — not persisted, not in memory, not asserted in a test (research.md R4). No input manifest exists anywhere in the tree, and no comparison harness exists anywhere in the repository." + }, + "overlay": [ + { + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"workspaces/alpha/src/**\",\"workspaces/alpha-tools/**\",\"workspaces/alpha.config/**\"]" + }, + { + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/**\",\"packages/core/**\",\"packages/cli/**\"]" + }, + { + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"**\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\"]" + }, + { + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"src/utils/**\",\"src/Utils/**\",\"src/README.md\"]" + }, + { + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"scripts/build-?.ts\",\"scripts/*.test.ts\",\"scripts/build.ts\"]" + }, + { + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"packages/core/src/**\",\"packages/core/**\"]" + }, + { + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"plugins/v2/**\",\"plugins/v10/**\",\"plugins/v_next/**\"]" + }, + { + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\".github/workflows/**\",\"Makefile\",\".github/**\"]" + }, + { + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"docs/**\",\"examples/**\",\"docs/**\"]" + }, + { + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[\"a/b/**\",\"a-b/**\",\"a/**\",\"a.b/**\"]" + }, + { + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "annotationValue": "[]" + } + ], + "overlayProvenance": { + "authorship": "maintainer-authored", + "entries": 23, + "ordering": "Ascending `compareCodeUnits` on `sourcePath`, with `documentIndexInFile` ascending as tiebreak. For this corpus that order coincides with the frozen selection order of selection-basis.md §3, because each of the 24 selected entities comes from a distinct workspace and no selected workspace name is a strict prefix of another.", + "statedPlainly": "Zero descriptors in the pinned corpus carry adrkit.io/owned-paths — R14 records it and this freeze re-derived it. Every annotation value is the maintainer's own, written over real upstream descriptors left otherwise unmodified. ADR-0020 clause 5 adopts ADR-0012 gate 3's named construction, \"synthetic explicit annotations over pinned public corpora under independent adversarial review\"; the independent adversarial review half is T019. Only the corpus data is third-party — never the validation (ADR-0014 honesty rules).", + "ownershipStatesExercised": { + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1 + }, + "clause5Floor": "Clause 5 requires at least one non-empty annotation on a real entity; this overlay carries 22, so the all-annotation-absent corpus clause 5 warns satisfies nothing is not what was frozen." + }, + "expectedPaths": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/acr/plugins/acr/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/adr/plugins/adr-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/agent-forge/plugins/agent-forge/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/airbrake/plugins/airbrake-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/allure/plugins/allure/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/analytics/plugins/analytics-module-ga4/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apache-airflow/plugins/apache-airflow/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/apollo-explorer/plugins/apollo-explorer/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-devops/plugins/azure-devops-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/azure-sites/plugins/azure-sites-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/badges/plugins/badges-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bazaar/plugins/bazaar-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitrise/plugins/bitrise/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/catalog/plugins/catalog-backend-module-codeowners/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/checkmarx/plugins/checkmarx-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cicd-statistics/plugins/cicd-statistics-module-buildkite/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/cloudbuild/plugins/cloudbuild/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-climate/plugins/code-climate/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/code-coverage/plugins/code-coverage-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/codescene/plugins/codescene/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "sourcePath": "workspaces/copilot/plugins/copilot-backend/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation IS present and IS a string scalar; it decodes via JSON.parse to an array of length zero. owned-paths-annotation.md §3 requires this to be a decoded-value check, never a raw-string equality check, and §4 requires it not to be confused with a single empty-string element, which would instead be rejected at the glob dialect's `empty` rule." + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "sourcePath": "workspaces/cost-insights/plugins/cost-insights-common/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [], + "//": "The annotation key is wholly absent — there is no overlay entry for this entity. owned-paths-annotation.md §1 step 1 decides presence by an explicit discriminant, never by inferring it from a raw value being undefined, and §3's non-conflation rule forbids treating this as equivalent to the explicit-empty entity above." + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bitbucket-pull-requests/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "sourcePath": "workspaces/bookmarks/plugins/bookmarks/examples/component/catalog-info.yaml", + "documentIndexInFile": 0, + "expectedPaths": [ + "docs/**" + ] + } + ], + "expectedPathsProvenance": { + "entries": 24, + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "howProduced": "By hand, by applying the frozen contracts to the maintainer-authored overlay values in overlay.json. Nothing here was produced by, checked against, or adjusted to match any generator. No generator exists at this point: no package has been created, and Phase E may not begin until T024.", + "disclosedArithmeticCheck": "Each `expectedPaths` array below was written by hand and then checked against `compareCodeUnits` — whose entire definition is `a < b ? -1 : a > b ? 1 : 0` over UTF-16 code units (packages/core/src/ordering/index.ts) — to confirm the hand ordering was not miscounted. This is disclosed rather than done silently. It is not generator output under research.md R4: a two-line comparator promoted into `@adrkit/core` is a frozen repository primitive, not the assembled generator, and it computes no ownership result. All eleven arrays and the 25-element union matched the hand-written values on the first check.", + "agreementWithTheOracle": "Every (canonicalId, expectedPaths) pair here is identical to the corresponding entry in frozen-expectations/frozen-expectation-set.json. The two artifacts are two views of one freeze, and a divergence between them is a freeze failure, not a discrepancy to be reconciled." + }, + "contentHash": "f98e6d464b53ba334298c1e5b76bbd0222ff2a460f7637439f0e8d54c9294aca", + "contentHashNotes": { + "algorithm": "Lowercase-hex SHA-256 over the canonical form defined in ../README.md §3: this top-level object with the `contentHash` key removed and nothing else removed, keys ordered ascending by compareCodeUnits, array order preserved, no insignificant whitespace, UTF-8, no trailing newline.", + "forTheAuditor": "Recompute it from this artifact. Do not copy it. data-model.md §16: \"An audit that transcribes the author's declared hash has verified nothing.\"" + }, + "warrantAndLimits": { + "whatThisFreezeBuys": "It fixes, in advance and in the open, what ownership derivation over this corpus is expected to produce. Per ADR-0020 clause 5 it exercises derivation against real descriptor structure and real field shapes, and it gates TECHNICAL COMPATIBILITY ONLY.", + "whatItDoesNotBuy": [ + "It does not evidence that the mapping reflects anyone's actual ownership.", + "It does not evidence that anyone else wants the annotation, and adoption remains entirely ungated.", + "It does not evidence that adoption risk has fallen.", + "It is not correctness evidence. A populated, digest-verified envelope would prove INTEGRITY, NOT CORRECTNESS — a semantically wrong envelope can carry a perfectly valid self-digest (ADR-0020 clause 5; spec.md FR-058; SC-012). Correctness is claimed only on FR-056 / SC-011, in Phase F, recording its own hashes and its own PASS/FAIL and inheriting nothing from this step.", + "It says nothing about Backstage as a running system. The admissibility warrant is exactly what the four pinned validator predicates return at Backstage commit 1121a4facd9e321179d0402c3f355e4a649e84d9.", + "ADR-0014 rung 1 only — not reference-verified, not externally validated, and no release is scheduled, prepared, or implied." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/accept-corpus-freeze/adequacy-audit.json b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/accept-corpus-freeze/adequacy-audit.json new file mode 100644 index 00000000..ed634e4d --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/accept-corpus-freeze/adequacy-audit.json @@ -0,0 +1,92 @@ +{ + "artifact": "adequacy-audit", + "schemaNote": "Independent T019 auditor adequacy finding for the accept corpus freeze (T018), plus the consolidated auditor verdict. Sibling to accept-corpus-freeze.json; deliberately NOT part of its canonical hash input.", + "task": "T019 (feature 010, Barrier B, ADR-0020 clause 6 oracle cycle, step T014a)", + "auditor": { + "role": "independent reviewer", + "authoringInvolvementInT014ToT018": false, + "baseCommitAudited": "4fff13d" + }, + "requirement3_adequacyFinding": { + "mandate": "ADR-0020 clause 5(a) requires an explicit adequacy judgement on the accept corpus; an integrity/hash confirmation alone does not satisfy it. The authoring session fixed size = 24 (one entity per eligible workspace, first 24 of 79 eligible workspaces in compareCodeUnits order) and stated its reasoning. This auditor judges that reasoning.", + "claimTheCorpusGates": "Technical compatibility only (ADR-0020 clause 5 step (a)): that the ownership-derivation contract can be exercised end-to-end against real Backstage descriptor structure and that the frozen oracle's ordering and dedup behaviour are the contract-admissible ones. The freeze explicitly does NOT claim scale ratification (ADR-0012 / FR-055), correctness of the generator, SC-010 satisfaction, or adoption. The adequacy judgement is scoped to that narrow claim.", + "coverageVerifiedIndependently": { + "ownershipStates": { + "explicit-paths": 22, + "explicit-empty": 1, + "annotation-absent": 1, + "note": "All three ownership states from the ownership model are present. explicit-paths carries >=2 instances (22); explicit-empty and annotation-absent carry exactly 1 each." + }, + "globFeatureCatalogue": "Positions 0-21 (the explicit-paths entities, in frozen selection order) realise 11 distinct deduped expectedPaths arrays, each carried by exactly two distinct canonical ids (a mod-11 assignment repeated across positions 0-10 and 11-21). Independently recomputed: exactly 11 distinct arrays, every one appearing exactly twice. Each documented ordering/dedup trap therefore has >=2 independent instances: uppercase-vs-lowercase (src/Utils vs src/utils), punctuation ordering (a/b, a-b, a., a/ family), duplicate-collapse (packages/core/** listed twice; docs/** repeated), numeric-vs-lexical version segments (v2/v10/v_next), and the bare '**' catch-all.", + "overlapNoExclusiveWinner": "The (acr, bazaar) pair share identical owned paths under distinct canonical ids, exercising entity-identity.md \u00a74's 'no exclusive winner' overlap case; the mod-11 mirroring gives 11 such overlap instances rather than one.", + "overlayEntityReconciliation": "23 overlay rows vs 24 entities reconciles exactly: 22 explicit-paths + 1 explicit-empty carry an annotation value (23 overlay rows); the single annotation-absent entity (cost-insights-common) has no annotation to overlay and correctly carries no overlay row. Not a defect." + }, + "reasoningJudged": { + "upperBound": "Reviewability. Expected paths are hand-authored so they can be checked line-by-line by a reviewer; a materially larger corpus would force code-derived expectations, which clause 6 constrains. This auditor accepts the upper bound as sound for a hand-verifiable oracle.", + "lowerBound": "Coverage-with-repetition: every ownership state and every catalogued glob/dedup feature exercised with >=2 independent instances so that a single transcription slip cannot silently pass. This auditor independently confirmed the >=2 repetition holds for all 11 catalogue entries and for each ordering trap. The lower bound is met by the data, not merely asserted.", + "selectionDeterminism": "Selection is 'first 24 of 79 eligible workspaces in compareCodeUnits order' \u2014 a deterministic, restatable rule fixed before entities were known (see pre-commitment finding below), not a hand-picked convenience sample." + }, + "finding": "ADEQUATE for the technical-compatibility claim it gates (ADR-0020 clause 5 step (a)).", + "reasoning": "Size 24 is adequate because it simultaneously (1) covers all three ownership states, (2) exercises every documented glob ordering/dedup trap with at least two independent instances, (3) provides 11 independent overlap/no-exclusive-winner instances, and (4) is selected by a deterministic, pre-committed rule rather than a convenience sample \u2014 while remaining small enough to be verified by hand, which the upper bound requires. The corpus is fit for the narrow purpose of demonstrating the contract runs against real descriptor structure and that the oracle's ordering is contract-admissible. This auditor did NOT invent a minimum count; per ADR-0012 and FR-055 a production/scale limit must be ratified from evidence, and this freeze explicitly disclaims making any scale-ratification claim. The adequacy judgement is therefore that 24 is sufficient for what is claimed and no more, NOT that 24 is a ratified production sample size.", + "boundedLimitation": "The pinned Backstage corpus (github.com/backstage/community-plugins @ 92e9e4e09c76cc57f3475029b73e5ec84498a459) is NOT checked out in this repository. This auditor could therefore NOT re-run the four FR-016 validator predicates or entity-identity canonicalisation against the actual descriptor files. The funnel figures (167 documents / 7 inadmissible / 2 colliding / 158 satisfying P / 79 eligible) were verified for internal arithmetic consistency, for agreement with research.md R14 and admissibility.md, and for cross-artifact consistency \u2014 but their ground truth against the pin is accepted on the authoring session's disclosed re-derivation, not independently reproduced. This is a bounded, disclosed limitation of the audit, not a defect in the freeze. It does not affect the adequacy finding, which turns on the structure and coverage of the 24 selected entities (fully inspectable here), not on the exact upstream funnel counts." + }, + "funnelReDerivation": { + "entityDocuments": 167, + "inadmissibleP4": 7, + "collidingP5": 2, + "satisfyingP": 158, + "arithmetic_167_minus_7_minus_2_equals_158": true, + "eligibleWorkspaces": 79, + "selectedCount": 24, + "selected_le_eligible": true, + "invalidNameSplitPreserved": "5 fail on character class + 2 fail on length alone = 7 documents with invalid metadata.name; the two populations ('invalid' vs 'over 63 chars') are kept distinct and NOT collapsed, matching research.md R14 and admissibility.md \u00a72.1.", + "crossArtifactAgreement": "All 24 entities' expectedPaths arrays are byte-identical between accept-corpus-freeze.json and frozen-expectation-set.json (0 missing, 0 mismatch), matched by canonicalId.", + "verdict": "PASS \u2014 internally consistent and cross-artifact consistent; ground-truth-against-pin not independently reproduced (see boundedLimitation)." + }, + "exclusionReasoning": { + "fivePlaceholders": { + "claim": "The 5 unsubstituted skeleton descriptors are excluded on INADMISSIBILITY, not collision \u2014 they carry '${{ values.name | dump }}' as metadata.name, fail isValidObjectName, and never acquire a canonical id.", + "checkedAgainst": "admissibility.md \u00a74.1 (an inadmissible descriptor never acquires a canonical id and cannot participate in a duplicate determination) and spec.md FR-020 (inadmissibility is the earlier, more specific defect). The 5 placeholders are a subset of the 7 P4-inadmissible documents.", + "verdict": "CONSISTENT." + }, + "nexusPair": { + "claim": "The Nexus pair (workspaces/nexus-repository-manager/.../catalog-info.yaml, two documents both canonicalising to component:default/backstage-community-nexus-repository-manager) is the residual VALID duplicate; BOTH documents are excluded, and that workspace contributes no entity.", + "checkedAgainst": "ADR-0015 Condition of Acceptance 3 (the irreducible valid duplicate that no admissibility rule can or should reach) and ADR-0020's identification of this pair as why spike-009 SC-010 was unsatisfiable under frozen inputs. Both documents are admissible, so exclusion is by collision (P5), not inadmissibility \u2014 correctly the opposite basis from the placeholders.", + "verdict": "CONSISTENT \u2014 the two exclusion mechanisms are correctly distinguished: placeholders never reach canonicalisation; the Nexus pair does and collides." + } + }, + "supplementaryChecks": { + "preCommitment": { + "claim": "Commit 654031b contains only the selection RULE (naming no selected entity); commit 4fff13d adds selection-basis.md \u00a78 with 124 insertions, 0 deletions, proving \u00a71-\u00a77 were untouched after entities were known.", + "verifiedBy": "git diff --numstat 654031b 4fff13d on selection-basis.md => 124 insertions, 0 deletions; the added block is entirely \u00a78; the rule commit names no selected canonical id.", + "verdict": "CONFIRMED independently." + }, + "barrierClaim": { + "claim": "No generator was run: no catalog-backstage adapter package, no generator-derived output, no SnapshotEnvelope, no input manifest, no comparison harness exists.", + "verifiedBy": "Inspecting the tree, not the assertion: no catalog-backstage package under packages/; no SnapshotEnvelope or InputManifest fields present anywhere under specs/010-catalog-backstage/evidence/; no comparison harness.", + "verdict": "CONFIRMED independently." + }, + "toolchain": { + "bunTest": "857 pass, 0 fail (after bun install \u2014 see note)", + "typecheck": "exit 0", + "adrLint": "checked 20 records, 0 errors, 0 warnings, exit 0", + "note": "On first run bun test showed 123 failures; root-caused to the worktree never having had `bun install` run (node_modules/@adrkit absent, CLI failing with 'Cannot find module @adrkit/core'). The tree was clean (no auditor changes) at that point, so this was purely an uninitialised-workspace state, not a defect in the artifacts and not caused by this audit. After `bun install` all 857 tests pass." + } + }, + "requirement4_auditorVerdict": { + "verdict": "PASS", + "inTheAuditorsWords": "As the independent T019 reviewer, I find the Barrier B oracle freeze sound for the technical-compatibility claim it makes. Both content hashes recompute exactly from the artifact bytes using a serializer I wrote from scratch (e641ae5e...c695c2430c and f98e6d46...c9294aca), so the recorded values are honest and the freeze is byte-stable. derivedPathPatterns is in compareCodeUnits order, not input order \u2014 the spike-009 defect is absent \u2014 and I confirmed this against the frozen comparator's actual semantics, including that a locale-aware sort would give a different, wrong answer for the deliberately-present case and punctuation data. The corpus of 24 is ADEQUATE for the narrow technical-compatibility claim: it covers every ownership state and every documented glob/dedup trap with at least two independent instances under a deterministic, pre-committed selection rule, while staying hand-verifiable. The pre-commitment and no-generator-run claims both hold under direct inspection. The one honest limitation is that the pinned upstream corpus is not checked out here, so the funnel counts are verified for consistency but not reproduced against ground truth; this does not affect the adequacy finding. I found no defect in the audited artifacts. I record PASS.", + "explicitNonClaims": [ + "This PASS does NOT clear Barrier B: T020-T024 of the oracle cycle remain.", + "This PASS does NOT assert ADR-0014 rung 2 or rung 3.", + "This PASS does NOT assert SC-010 is satisfied beyond the technical-compatibility step this freeze gates.", + "This PASS does NOT ratify 24 (or any number) as a production/scale sample size \u2014 no minimum count was invented, per ADR-0012 and FR-055." + ], + "defectsFound": [], + "concernsAndLimitations": [ + "BOUNDED LIMITATION (disclosed): the pinned Backstage corpus is not checked out, so the four validator predicates and canonicalisation could not be re-run against ground truth; funnel figures were verified for internal/cross-artifact consistency only.", + "RESIDUAL NON-FALSIFIABILITY (clause 6): the disclosed 'hand-derived then checked against compareCodeUnits' process is not falsifiable from the artifact bytes alone; judged CONSISTENT with clause 6 because the admissible ordering is uniquely fixed by the owned-paths-annotation.md \u00a73 contract, leaving no room to game the oracle, so the gaming risk the clause guards against is nil regardless.", + "ENVIRONMENTAL (not a defect): the worktree required `bun install` before the test suite would pass; recorded for reproducibility." + ] + } +} diff --git a/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/frozen-expectations/frozen-expectation-set.json b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/frozen-expectations/frozen-expectation-set.json new file mode 100644 index 00000000..5330aa4a --- /dev/null +++ b/specs/010-catalog-backstage/evidence/negative-cases/oracle-input-order/frozen-expectations/frozen-expectation-set.json @@ -0,0 +1,275 @@ +{ + "//": "T017 — the re-frozen oracle. FrozenExpectationSet, data-model.md §16. This is the fresh T014 step of ADR-0020 clause 6's T014 → T014a cycle. The T014a step (the independent audit) is T019 and is recorded in the sibling file audit-record.json, which does not exist yet and must not be written by the author of this file.", + "task": "T017", + "barrierSide": "IS THE BARRIER", + "discharges": [ + "FR-053" + ], + "frozenAt": "2026-08-05T03:43:58Z", + "correction": { + "whatWasWrong": "Spike 009 froze `derivedPathPatterns` in INPUT order. ADR-0012 gate 3 records the consequence: 'The oracle exists but carries a known-wrong expected result and must be re-frozen.'", + "whatIsRightAndWhyItIsRight": "`derivedPathPatterns` below is in `compareCodeUnits`-sorted order. data-model.md §16 names this correction directly, and it follows from owned-paths-annotation.md §3, which requires an `explicit-paths` derivation to be sorted and deduplicated. An oracle recording input order is asserting an expectation the derivation contract forbids.", + "whyReuseWasNotAnOption": "ADR-0020 clause 6 requires a FRESH cycle — correct the ordering, re-freeze, re-hash, obtain a NEW independent pre-output audit — rather than an amendment to the spike's artifact. ADR-0015's Condition of Acceptance 1 records why: the spike's evidence bundle is untracked and scratch-only, so 'nothing in the repository will stop someone reusing a stale copy.' Nothing in this file is copied from, or reconciled against, that bundle.", + "howAnAuditorFalsifiesTheOrdering": "Sort `derivedPathPatterns` with `compareCodeUnits` (`a < b ? -1 : a > b ? 1 : 0`, packages/core/src/ordering/index.ts) and compare element-by-element with the array as recorded. They must be identical. Then confirm the array is NOT merely the concatenation of the overlay's annotation values in authored order — for this corpus the two disagree at the first element (`**` sorted, `workspaces/alpha/src/**` in input order), so the distinction is visible immediately rather than only in the tail." + }, + "derivedPathPatterns": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**", + "packages/cli/**", + "packages/core/**", + "**", + ".github/**", + "docs/**", + "src/README.md", + "src/Utils/**", + "src/utils/**", + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts", + "packages/core/src/**", + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**", + ".github/workflows/**", + "Makefile", + "examples/**", + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ], + "derivedPathPatternsProvenance": { + "definition": "The deduplicated union of every pattern appearing in any entity's `expectedPaths`, in ascending `compareCodeUnits` order.", + "count": 25, + "sourceOfTruth": "accept-corpus-freeze/expected-paths.json, itself hand-derived from accept-corpus-freeze/overlay.json under the frozen contracts. Not produced by any generator; none exists.", + "orderingTrapsItEncodes": [ + "`*` (0x2A) precedes `.` (0x2E), so a bare `**` sorts before `.github/**`.", + "`M` (0x4D) precedes every lowercase letter, so `Makefile` sorts before `a-b/**`.", + "`-` (0x2D) < `.` (0x2E) < `/` (0x2F), so `a-b/**` and `a.b/**` both precede `a/**`.", + "`*` precedes letters within a shared prefix, so `a/**` precedes `a/b/**` and `packages/core/**` precedes `packages/core/src/**`.", + "digits sort lexically, so `plugins/v10/**` precedes `plugins/v2/**`; `_` (0x5F) follows digits, so `plugins/v_next/**` is last of the three.", + "uppercase precedes lowercase, so `src/README.md` < `src/Utils/**` < `src/utils/**`." + ] + }, + "expectedByEntity": [ + { + "canonicalId": "component:default/backstage-community-acr", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-adr-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-agent-forge", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-airbrake-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-allure", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-analytics-module-ga4", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apache-airflow", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-apollo-explorer", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-devops-common", + "ownershipState": "explicit-paths", + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-azure-sites-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-badges-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bazaar-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "workspaces/alpha-tools/**", + "workspaces/alpha.config/**", + "workspaces/alpha/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-bitrise", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "**", + ".github/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-catalog-backend-module-codeowners", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "src/README.md", + "src/Utils/**", + "src/utils/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-checkmarx-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "scripts/*.test.ts", + "scripts/build-?.ts", + "scripts/build.ts" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cicd-statistics-module-buildkite", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/core/**", + "packages/core/src/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-cloudbuild", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "plugins/v10/**", + "plugins/v2/**", + "plugins/v_next/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-climate", + "ownershipState": "explicit-paths", + "expectedPaths": [ + ".github/**", + ".github/workflows/**", + "Makefile" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-code-coverage-backend", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**", + "examples/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-codescene", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "a-b/**", + "a.b/**", + "a/**", + "a/b/**" + ] + }, + { + "canonicalId": "component:default/backstage-plugin-copilot-backend", + "ownershipState": "explicit-empty", + "expectedPaths": [] + }, + { + "canonicalId": "component:default/backstage-plugin-cost-insights-common", + "ownershipState": "annotation-absent", + "expectedPaths": [] + }, + { + "canonicalId": "component:default/bitbucket-pull-requests", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "packages/cli/**", + "packages/core/**" + ] + }, + { + "canonicalId": "component:default/example-website", + "ownershipState": "explicit-paths", + "expectedPaths": [ + "docs/**" + ] + } + ], + "expectedByEntityNotes": { + "ordering": "Ascending `compareCodeUnits` on `canonicalId`.", + "count": 24, + "whyOwnershipStateIsPresent": "data-model.md §16 sketches `expectedByEntity` as `{ canonicalId, expectedPaths }`. `ownershipState` is carried in addition because owned-paths-annotation.md §3's non-conflation rule requires the discriminator to be an explicit field: `explicit-empty` and `annotation-absent` both yield an empty array, and the rule forbids inferring the distinction from `derivedPaths` alone. An oracle recording only the two sketched fields could not express a distinction the derivation contract mandates. The addition is a superset of §16's shape and removes nothing from it.", + "whyAuditRecordIsNotHere": "data-model.md §16 gives FrozenExpectationSet an `auditRecord` member. It is deliberately absent from this file. Writing the audit into the artifact it audits would change that artifact's bytes and therefore its hash, so the recorded hash could never match a re-derivation. The audit is written by T019 to the sibling file `audit-record.json`, which is also what T019's own task line specifies. The composed §16 type is the pair taken together; `contentHash` covers the frozen half." + }, + "contentHash": "9c53886e8fcfba451e1b338d229f85859a2a6b3b8691b5b836d1e90aaaf7d5ea", + "contentHashNotes": { + "algorithm": "Lowercase-hex SHA-256 over the canonical form defined in ../README.md §3: this top-level object with the `contentHash` key removed and nothing else removed, serialized with object keys ordered ascending by `compareCodeUnits`, array order preserved, no insignificant whitespace, UTF-8, no trailing newline.", + "forTheAuditor": "Recompute it. Do not copy it. data-model.md §16 states the rule for a reason: 'An audit that transcribes the author's declared hash has verified nothing.'" + }, + "warrantAndLimits": { + "whatThisArtifactIs": "A statement of what the derivation is EXPECTED to produce, fixed before any generator exists.", + "whatItIsNot": [ + "It is not evidence that any generator produces these values. No generator has been written or run.", + "It is not correctness evidence. Correctness is claimed only on FR-056 / SC-011's post-output comparison at zero false positives and zero false negatives, which is a separate step in Phase F recording its own hashes and its own PASS/FAIL and inheriting nothing from this one.", + "It is not a claim about Backstage as a running system.", + "It is ADR-0014 rung 1 only — not reference-verified, not externally validated. Only the corpus data is third-party; this oracle is the maintainer's own." + ] + } +} diff --git a/specs/010-catalog-backstage/tasks.md b/specs/010-catalog-backstage/tasks.md index 6a0b2f5a..139c1926 100644 --- a/specs/010-catalog-backstage/tasks.md +++ b/specs/010-catalog-backstage/tasks.md @@ -288,7 +288,7 @@ matches, and the recorded selection basis and size to be frozen in the **same cy with the audit recording its own hashes and its own PASS/FAIL. Phase B may run concurrently with Phase A and with nothing else. -- [ ] T013 [US1] Create the tracked evidence tree — `/README.md`, +- [X] T013 [US1] Create the tracked evidence tree — `/README.md`, `/frozen-expectations/`, `/accept-corpus-freeze/`, and `/negative-cases/`. **`negative-cases/` is a SHARED, CROSS-PHASE tree.** Roughly twenty tasks spanning @@ -303,7 +303,7 @@ concurrently with Phase A and with nothing else. Discharges: none — enables FR-053, FR-054, FR-055 Depends: none -- [ ] T014 [US1] **Record the accept-corpus selection basis and size before acting on +- [X] T014 [US1] **Record the accept-corpus selection basis and size before acting on it.** Write `/accept-corpus-freeze/selection-basis.md` stating how the corpus was chosen and how large it is, and how the populations documented in `research.md` R14 were handled — specifically the invalid-`metadata.name` @@ -314,14 +314,14 @@ concurrently with Phase A and with nothing else. Discharges: FR-055 Depends: T013 -- [ ] T015 [US1] Author the maintainer-authored `adrkit.io/owned-paths` overlay at +- [X] T015 [US1] Author the maintainer-authored `adrkit.io/owned-paths` overlay at `/accept-corpus-freeze/overlay.json`. This content is maintainer-authored, never upstream-authored, and the record must say so. Barrier: IS THE BARRIER Discharges: FR-054 (overlay half) Depends: T014 -- [ ] T016 [US1] Author the expected path matches per canonical id at +- [X] T016 [US1] Author the expected path matches per canonical id at `/accept-corpus-freeze/expected-paths.json`. These are **hand-derived from the frozen contracts**, never produced by, checked against, or adjusted to match any generator — no generator exists at this point, and Phase E may not @@ -330,7 +330,7 @@ concurrently with Phase A and with nothing else. Discharges: FR-054 (expected-paths half) Depends: T015 -- [ ] T017 [US1] **Re-freeze the oracle (the fresh T014 step).** Write +- [X] T017 [US1] **Re-freeze the oracle (the fresh T014 step).** Write `/frozen-expectations/frozen-expectation-set.json` containing `derivedPathPatterns` in `compareCodeUnits`-sorted order — this ordering is the correction the fresh cycle exists to make; input order is the defect — plus @@ -339,7 +339,7 @@ concurrently with Phase A and with nothing else. Discharges: FR-053 Depends: T014 -- [ ] T018 [US1] Assemble `/accept-corpus-freeze/accept-corpus-freeze.json` +- [X] T018 [US1] Assemble `/accept-corpus-freeze/accept-corpus-freeze.json` — `corpusRef`, `selectionBasis`, `size`, `overlay`, `expectedPaths`, `contentHash` — **in the same cycle** as T014–T017. This artifact and the T017 oracle are frozen together or not at all. @@ -347,7 +347,7 @@ concurrently with Phase A and with nothing else. Discharges: FR-054 (same-cycle freeze) Depends: T015, T016, T017 -- [ ] T019 [US1] **The independent audit (the T014a step).** A reviewer with no +- [X] T019 [US1] **The independent audit (the T014a step).** A reviewer with no authoring involvement in T014–T018 **recomputes** both content hashes from the artifacts themselves — never copies the recorded values — confirms the `derivedPathPatterns` ordering is `compareCodeUnits` and not input order, records @@ -359,7 +359,7 @@ concurrently with Phase A and with nothing else. Discharges: FR-057 (step (a) half), SC-010 Depends: T018 -- [ ] T020 [US1] **Observed failing.** Construct an oracle variant whose +- [x] T020 [US1] **Observed failing.** Construct an oracle variant whose `derivedPathPatterns` are in input order rather than `compareCodeUnits` order; run the T019 audit against it; observe the audit return FAIL and record the exact reason; restore the correct artifact; observe PASS. Retain the failing @@ -368,7 +368,7 @@ concurrently with Phase A and with nothing else. Discharges: none — supplies the ADR-0016 observation for FR-053 Depends: T019 -- [ ] T021 [US1] **Observed failing.** Construct an audit run that confirms hash +- [x] T021 [US1] **Observed failing.** Construct an audit run that confirms hash integrity but never reaches an adequacy finding; observe it recorded as FAIL against SC-010 rather than silently accepted; restore; observe PASS. Retain at `/negative-cases/audit-integrity-only/`. @@ -376,7 +376,7 @@ concurrently with Phase A and with nothing else. Discharges: none — supplies the ADR-0016 observation for SC-010 Depends: T019, T020 -- [ ] T022 [US1] Build the CI freeze-hash drift check — **R5 mechanism 2**. It +- [x] T022 [US1] Build the CI freeze-hash drift check — **R5 mechanism 2**. It re-derives the content hashes of everything under `/frozen-expectations/` and `/accept-corpus-freeze/` and fails the build on any drift. Files: `scripts/check-freeze-hashes.ts`, `scripts/check-freeze-hashes.test.ts`, @@ -385,7 +385,7 @@ concurrently with Phase A and with nothing else. Discharges: none — implements R5 mechanism 2 Depends: T019 -- [ ] T023 [US1] **Observed failing.** Mutate a single byte of one frozen artifact; +- [x] T023 [US1] **Observed failing.** Mutate a single byte of one frozen artifact; run the T022 check; observe it fail and record the exact reason; restore the byte; observe the pass. Files: `scripts/check-freeze-hashes.test.ts`, @@ -394,7 +394,7 @@ concurrently with Phase A and with nothing else. Discharges: none — supplies the ADR-0016 observation for R5 mechanism 2 Depends: T022 -- [ ] T024 [US1] **BARRIER B CHECKPOINT — HARD GATE.** Confirm and record all three +- [x] T024 [US1] **BARRIER B CHECKPOINT — HARD GATE.** Confirm and record all three R5 mechanisms simultaneously: **(1) input absence** — no input manifest exists anywhere in the tree, and the adapter contains no recursive walking or glob discovery that could substitute for