feat(catalog): feature 010 Phase A — workspace placement and dependency boundary - #84
Merged
Merged
Conversation
…cy boundary Implements T001–T008 of `specs/010-catalog-backstage/tasks.md`, and carries out the T009–T012 observed-failing runs whose enforcement half lands here. Two new packages, placed per `contracts/package-boundary.md` §1: - `packages/adapters/catalog-backstage/` (`@adrkit/catalog-backstage`), independently versioned per ADR-0007 with the `"//versioning"` note §6 requires, since a reader seeing its `@adrkit/core` dependency would otherwise infer coupled versioning and be wrong. - `packages/catalog-envelope/` (`@adrkit/catalog-envelope`), deliberately not under `packages/adapters/`, so `isAdapterPackage()` classifies it as a non-adapter by location rather than by exception (§3.1). Both are picked up by the existing root `workspaces` globs with no change to them, which §1 requires: a needed change there would signal wrong placement. Guards, each observed rejecting before being relied on (ADR-0016): - `test/no-dynamic-loader.test.ts` (FR-002) — seven runtime-resolution and registry rules over the adapter source, plus a positive static-import assertion on a specific value rather than on the absence of a loader. - `test/envelope-shape-locality.test.ts` (FR-005, FR-044) — no reference to the published schema surface, no import edge between the two packages, no relative import escaping either package root, and no import of an undeclared module. The "shape is declared locally" half is enforced by those invariants rather than by counting declarations, which would pass vacuously today. - `scripts/check-deps.ts` — explicit `allowedDependenciesFor()` entries for both packages (§2). The `@adrkit/cli` entry is untouched, per §7. - `scripts/check-deps.test.ts` — the retained negative cases for the core→adapter, cli→adapter, consumer→adapter, adapter→consumer, and disallowed-dependency edges, plus the §4 trap itself: a package with no allowlist entry passes with the same disallowed dependency, which is what makes the others mean anything. Both scans report the files they read and refuse to conclude anything from an empty file list, and the excluded-from-scan set is pinned so it cannot grow quietly. Two findings recorded in the tests rather than left as assumptions: - `check:deps` cannot see `schema/` — it has no manifest, so FR-003's `schema/` clause is held by `bunfig.toml`'s isolated linker and `typecheck` (TS2307), not by this check. T009's third case is therefore not satisfiable as written. - The lockfile's `@adrkit/spec-kit` version moves 0.1.0 → 0.1.2, correcting pre-existing staleness; that manifest already read 0.1.2 at HEAD and is unmodified here. T009–T012 are left unchecked: their `Files` lines also name `specs/010-catalog-backstage/evidence/negative-cases/`, a tree owned by the concurrent Phase B session and not written here. ADR-0014 rung 1 only. No rung-2 or rung-3 claim; no release scheduled or prepared (ADR-0020 clause 9). Refs: specs/010-catalog-backstage/tasks.md T001–T012 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Completes T009–T012 by depositing the retained failing inputs ADR-0016 clause 2 asks for, into the four subdirectories those tasks name. Creates `specs/010-catalog-backstage/evidence/negative-cases/`, which no task creates. Each case directory is self-describing: a `.patch` carrying the failing input, verbatim `.observed.txt` captures written by the run rather than transcribed from it, the restored-pass observation, and a README naming the command, the exact emitted strings, and the exit code. All nine patches were round-tripped — applied, re-run, and confirmed to reproduce their documented exit codes. Observed against 1014972, Bun 1.3.14, TypeScript 6.0.3. - `dep-core-to-adapter/` (T009, SC-015) — core→adapter and cli→adapter both fail `check:deps` at exit 1. The third surface, `schema/`, does **not**: it carries no `package.json`, so `readWorkspacePackages()` never visits it and `check:deps` exits 0 printing `core-has-no-adapter-deps: ok`. That edge is rejected by `bun run typecheck` instead, with `TS2307: Cannot find module '@adrkit/catalog-backstage'`, because the isolated linker leaves root-level files no `node_modules/@adrkit/` to resolve through — Constitution Principle III's stated reason the setting is load-bearing, observed working. Recorded under the command that genuinely produces it, with the distinction stated rather than smoothed over. - `dep-consumer-to-adapter/` (T010, FR-044 i) — two guards fire, not the one the task names; both recorded. The non-adapter violation is also positive mechanical confirmation that the consumer is placed outside `packages/adapters/`, since that reason can only be emitted for a package that is. - `dep-adapter-to-consumer/` (T011, FR-044 ii) — exactly one guard fires, since the adapter *is* an adapter and the non-adapter guard's condition excludes it. In this direction the allowlist entry is the only thing holding the boundary. - `dep-allowlist-present/` (T012) — `undici` rejected in each package independently, plus the §4 trap: with the entry removed and `undici` still declared, `check:deps` exits 0, and its capture is **byte-identical** to the clean-tree capture (verified with `diff`). That identity is what makes the first two cases mean anything. Also recorded: T008 shifted the two guards from lines 175–182 and 196–204 to 216–224 and 236–244, so the line citations in tasks.md and `contracts/package-boundary.md` §4 are stale. The emitted reasons are unchanged. `negative-cases/README.md` documents the per-subdirectory convention and is deliberately not an index, since the tree is shared across phases and an enumeration would go stale. Untouched, as they belong to the concurrent Phase B session: `<EVIDENCE>/README.md`, `<EVIDENCE>/frozen-expectations/`, `<EVIDENCE>/accept-corpus-freeze/`. Verification: `bun test` 904 pass / 0 fail, `bun run typecheck` clean, `bun run check:deps` ok, `bun run adr lint` 20 records / 0 errors / 0 warnings. ADR-0014 rung 1 only. No rung-2 or rung-3 claim; no release scheduled or prepared (ADR-0020 clause 9). Refs: specs/010-catalog-backstage/tasks.md T009–T012 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Both captures showed only bun's own `$ bun run scripts/check-deps.ts` wrapper echo, while every other capture in the tree opens with the invoked command `$ bun run check:deps`. The negative-cases convention requires each capture to name the command that produced it, because `check:deps` and `typecheck` are not interchangeable here — case 3 in this same directory passes under one and fails under the other. Re-captured by re-running each observation rather than editing the files, so they remain verbatim output rather than transcription. Both still exit 1 with the same two reason strings; only the header line is new. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Introduces Phase A scaffolding and dependency boundaries for the Backstage catalog adapter and envelope consumer.
Changes:
- Adds two unreleased workspace package skeletons.
- Adds dependency and source-boundary guards with tests.
- Records negative-case evidence and completes T001–T012 checkboxes.
Reviewed changes
Copilot reviewed 41 out of 42 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
specs/010-catalog-backstage/tasks.md |
Marks Phase A tasks complete. |
specs/010-catalog-backstage/evidence/negative-cases/README.md |
Defines evidence conventions. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/restored.observed.txt |
Records restored checks. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/README.md |
Documents core, CLI, and schema cases. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/case-3-schema-to-adapter.patch |
Captures schema violation input. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/case-3-schema-to-adapter.observed.txt |
Records schema-case output. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/case-2-cli-to-adapter.patch |
Captures CLI violation input. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/case-2-cli-to-adapter.observed.txt |
Records CLI-case output. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/case-1-core-to-adapter.patch |
Captures core violation input. |
specs/010-catalog-backstage/evidence/negative-cases/dep-core-to-adapter/case-1-core-to-adapter.observed.txt |
Records core-case output. |
specs/010-catalog-backstage/evidence/negative-cases/dep-consumer-to-adapter/restored.observed.txt |
Records restored consumer check. |
specs/010-catalog-backstage/evidence/negative-cases/dep-consumer-to-adapter/README.md |
Documents consumer-to-adapter case. |
specs/010-catalog-backstage/evidence/negative-cases/dep-consumer-to-adapter/observed.txt |
Records rejected dependency. |
specs/010-catalog-backstage/evidence/negative-cases/dep-consumer-to-adapter/dep-consumer-to-adapter.patch |
Captures dependency violation. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/restored.observed.txt |
Records restored allowlist check. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/README.md |
Documents allowlist trap cases. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/case-c-entry-removed-trap.patch |
Removes adapter allowlist entry. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/case-c-entry-removed-trap.observed.txt |
Records silent allowlist trap. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/case-b-consumer-disallowed-dep.patch |
Adds forbidden consumer dependency. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/case-b-consumer-disallowed-dep.observed.txt |
Records consumer rejection. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/case-a-adapter-disallowed-dep.patch |
Adds forbidden adapter dependency. |
specs/010-catalog-backstage/evidence/negative-cases/dep-allowlist-present/case-a-adapter-disallowed-dep.observed.txt |
Records adapter rejection. |
specs/010-catalog-backstage/evidence/negative-cases/dep-adapter-to-consumer/restored.observed.txt |
Records restored adapter check. |
specs/010-catalog-backstage/evidence/negative-cases/dep-adapter-to-consumer/README.md |
Documents adapter-to-consumer case. |
specs/010-catalog-backstage/evidence/negative-cases/dep-adapter-to-consumer/observed.txt |
Records rejected dependency. |
specs/010-catalog-backstage/evidence/negative-cases/dep-adapter-to-consumer/dep-adapter-to-consumer.patch |
Captures dependency violation. |
scripts/check-deps.ts |
Adds package dependency allowlists. |
scripts/check-deps.test.ts |
Tests catalog dependency boundaries. |
packages/catalog-envelope/tsconfig.json |
Configures envelope TypeScript sources. |
packages/catalog-envelope/src/index.ts |
Adds the consumer entry point. |
packages/catalog-envelope/README.md |
Documents status and boundaries. |
packages/catalog-envelope/package.json |
Defines the consumer workspace package. |
packages/adapters/catalog-backstage/tsconfig.json |
Configures adapter TypeScript sources. |
packages/adapters/catalog-backstage/test/source-scan.ts |
Adds source-scanning helpers. |
packages/adapters/catalog-backstage/test/no-dynamic-loader.test.ts |
Guards against dynamic loaders. |
packages/adapters/catalog-backstage/test/envelope-shape-locality.test.ts |
Guards schema and package locality. |
packages/adapters/catalog-backstage/src/index.ts |
Adds the adapter entry point. |
packages/adapters/catalog-backstage/README.md |
Documents adapter status and constraints. |
packages/adapters/catalog-backstage/package.json |
Defines the adapter workspace package. |
packages/adapters/catalog-backstage/NOTICE |
Adds package notices. |
packages/adapters/catalog-backstage/LICENSE |
Adds the Apache-2.0 license. |
bun.lock |
Registers both new workspaces. |
Suppressed comments (2)
packages/adapters/catalog-backstage/test/source-scan.ts:147
- The scan silently ignores executable
.js,.jsx,.tsx,.mts, and.ctsfiles even though the root TypeScript configuration allows JavaScript and nothing prevents these files from being added to the package. A dynamic loader placed in one of those files would bypass FR-002 while this guard remains green. Scan every executable source extension or explicitly fail when an unsupported source file is present.
for (const entry of entries) {
if (!entry.endsWith('.ts')) continue;
const absolute = join(base, entry);
const path = displayPath(relative(REPO_ROOT, absolute));
if (EXCLUDED_FROM_SCAN.includes(path)) continue;
packages/adapters/catalog-backstage/test/source-scan.ts:205
- This helper omits literal dynamic imports. Consequently, consumer code such as
await import('../../adapters/catalog-backstage/src/index.ts')is invisible to bothescapingRelativeImports()andundeclaredImports(), bypassing the claimed build-time/runtime no-edge boundary. Parse dynamicimport('…')specifiers too and retain a negative case for a relative dynamic import from the consumer.
* Matched on comment-stripped source. Covers `from '…'` (import and re-export,
* including multi-line forms) and side-effecting `import '…'`.
*/
export function importSpecifiers(code: string): string[] {
const specifiers: string[] = [];
for (const pattern of [/\bfrom\s*['"]([^'"]+)['"]/g, /\bimport\s*['"]([^'"]+)['"]/g]) {
| Contract: `package-boundary.md` §2, §4 | ||
|
|
||
| - [ ] T009 [US9] **Observed failing.** Introduce a dependency edge from `@adrkit/core` | ||
| - [X] T009 [US9] **Observed failing.** Introduce a dependency edge from `@adrkit/core` |
Comment on lines
+342
to
+346
| // The clause is held instead by `bunfig.toml`'s `linker = "isolated"` — root | ||
| // level files get no `node_modules/@adrkit/`, so the same edge fails | ||
| // `bun run typecheck` with `TS2307: Cannot find module | ||
| // '@adrkit/catalog-backstage'`. That is Constitution Principle III's stated | ||
| // reason the isolated linker is load-bearing, observed working. |
Comment on lines
+101
to
+105
| `packages/core`, `packages/cli`, and `schema/` import nothing from it and must | ||
| not otherwise learn it exists | ||
| ([ADR-0007](../../../docs/adr/0007-adapter-isolation-and-public-surface-build.md); | ||
| Constitution Principle III). Enforced by `bun run check:deps`, whose guard was | ||
| observed rejecting exactly that edge before it was relied on |
Comment on lines
+67
to
+72
| * Known limitation, stated rather than hidden: a regular-expression literal | ||
| * containing an unescaped `//`, or a lone quote character inside one, can put | ||
| * this scanner into the wrong state for the rest of that construct. The failure | ||
| * direction is a false negative. It is accepted because the alternative is a | ||
| * parser, and because {@link scanned} additionally asserts stripping never | ||
| * empties a non-empty file. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements T001–T012 of
specs/010-catalog-backstage/tasks.md— the whole of Phase A. All twelve boxes are checked; thetasks.mddiff is twelve checkboxes and nothing else.Standing honesty statement
ADR-0014 rung 1 only. Nothing here is
reference-verified(rung 2) orexternally validated(rung 3), and no file claims either. Any verification below is maintainer-owned, which is not external, third-party, or community adoption.No release is scheduled, implied, or prepared. ADR-0020 clause 9 defers both the release vehicle and the decision to release at all. Both packages sit at version
0.0.0, are absent fromRELEASE_PACKAGES, ship nodist, and declare noexportsmap.This feature has produced no evidence beyond the negative cases in this PR. No generator exists, no generator has run, and no envelope exists. Every behavioural statement in the specs remains a requirement, not a report.
Barrier B has not cleared. That is Phase B plus the independent T019 audit, running separately. Nothing here is behind the barrier: Phase A reads no descriptor, computes no ownership result, and produces no envelope.
What lands
Two packages, placed per
contracts/package-boundary.md§1:@adrkit/catalog-backstagepackages/adapters/catalog-backstage/@adrkit/catalog-envelopepackages/catalog-envelope/Both are picked up by the existing root
workspacesglobs with no change to them, which §1 requires — a needed change there would signal wrong placement. The rootpackage.jsondiff is empty.Guards, each observed rejecting before being relied on (ADR-0016):
test/no-dynamic-loader.test.ts(FR-002) — seven runtime-resolution and registry rules, plus a positive static-import assertion on a specific value rather than on the absence of a loader.test/envelope-shape-locality.test.ts(FR-005, FR-044) — no reference to the published schema surface, no import edge between the two packages, no relative import escaping either package root, no import of an undeclared module.scripts/check-deps.ts— explicitallowedDependenciesFor()entries for both packages (§2). The@adrkit/clientry is untouched, per §7.scripts/check-deps.test.ts— nine retained negative cases.Both scans report the files they read and refuse to conclude anything from an empty file list; the excluded-from-scan set is pinned so it cannot grow quietly.
All nine deposited patches were round-tripped — applied, re-run, confirmed to reproduce their documented exit codes, restored. The "Reproducing" sections in each deposit are therefore verified rather than asserted.
Five things a reviewer most needs to see
1.
check:depsstructurally cannot seeschema/— and T009 is checked anyway, on SC-015's wordingFR-003 covers three surfaces:
packages/core,packages/cli, andschema/. The first two failbun run check:depsat exit 1. The third does not.schema/has nopackage.json.readWorkspacePackages()walkspackages/and reads manifests, so a directory with no manifest is never visited. With a realimport … from '@adrkit/catalog-backstage'added toschema/adr.schema.ts:That edge is rejected by
bun run typecheckinstead:because
bunfig.toml'slinker = "isolated"leaves root-level files no@adrkit/scope to resolve through —ls node_modules/@adrkit/returnsNo such file or directory. That is Constitution Principle III's own stated reason the setting is load-bearing, observed doing the work it is there to do.T009 is checked on SC-015's wording, with the deviation named. SC-015 reads "from
packages/core,packages/cli, or theschema/surface … causes the isolation check to fail" — the isolation check, notcheck:depsspecifically. All three surfaces are observed failing, retained, and observed passing on removal. The deviation is that one of the three is discharged bytypecheckrather thancheck:deps, recorded prominently in the deposit rather than smoothed over. T009's own instruction to "runbun run check:deps; observe the failure" is not satisfiable forschema/, and no change tocheck-deps.tswould make it so without teaching the script to scan sources rather than manifests.A test pins this as a specific observed value ("does NOT see a manifest-less directory such as schema/, and this records that limitation"), so nobody later assumes coverage that does not exist.
2. §4's trap has a level above it
contracts/package-boundary.md§4 records that a package with no allowlist entry is silently unconstrained —allowedDependenciesFor()returnsundefinedand the allowed-surface guard is skipped, so it passescheck:depsno matter what it declares.The
schema/case is the same shape one level up: a directory with no manifest is invisible outright. In both, absence of a rule and a satisfied rule render as the same green string.A reader who assumes
check:depscoversschema/would be wrong — and wrong in the direction of a check that cannot fail, which is the only direction that matters in a governance tool.3. T012 case C is byte-identical to a clean run
With the adapter's allowlist entry removed and
undici— a network client — still declared:case-c-entry-removed-trap.observed.txtandrestored.observed.txtare byte-identical, verified withdiffrather than asserted. There is no signal to read.This is what makes cases A and B mean anything: without it, an exit-1 could not distinguish "the entry exists and rejected
undici" from "some other guard happened to reject it." Case C removes only the entry, holds everything else fixed, and the violation disappears.The honest consequence, stated in the deposit:
restored.observed.txtalone evidences nothing. It is identical to the trap's output. It is meaningful only alongside A and B, which show the same command does distinguish an allowed surface from a disallowed one while the entries are present.4. T010 fires two guards, and the extra one confirms placement
T010 names one guard. Two fire, and both are recorded:
The first is more than a duplicate finding.
isAdapterPackage()classifies purely by path prefix, sonon-adapter workspace depends on an adapter packagecan only be emitted for a package located outsidepackages/adapters/. Its appearance is therefore positive mechanical confirmation that the consumer is placed correctly — the FR-044 placement half, observed rather than asserted.Had the consumer been created under
packages/adapters/, this line would simply be absent, and the misplacement silent. That is the same failure shape as everything else in this PR, which is why it is recorded rather than treated as noise.The mirror case is worth reading beside it: T011 fires exactly one guard, correctly, since the non-adapter guard's condition is
!adapterPackageand the adapter is one. So in the adapter→consumer direction the allowlist entry is the only thing holding the boundary — which is precisely why case C matters.5. Stale line-number citations — a follow-up for the maintainer, not fixed here
T008's two allowlist entries shifted everything below them by +41:
non-adapter workspace depends on an adapter package<name> declares a dependency outside its allowed public surfaceallowedDependenciesFor()returnsundefinedEmitted reasons unchanged; only the numbers moved.
contracts/package-boundary.md§4 is normative and currently points at non-guard lines.Deliberately not fixed in this PR. Task text and the contract are the maintainer's to reconcile centrally, alongside the other findings, once the concurrent Phase B session lands. The intended fix is to replace the line-number citations with the emitted reason strings: the strings are stable across edits and are already asserted verbatim by
scripts/check-deps.test.ts, so a string citation is checked by the suite — whereas a line number is checked by nobody, and is itself a reference that cannot fail.Evidence deposits
specs/010-catalog-backstage/evidence/negative-cases/— created here, because no task creates it. Four subdirectories, one per case, each self-describing: a.patchcarrying the failing input, verbatim.observed.txtcaptures written by the run rather than transcribed from it, the restored-pass observation, and a README naming the command, exact strings, and exit code.Every capture opens with the command that produced it, because
check:depsandtypecheckare not interchangeable here — case 1 above passes under one and fails under the other. Two captures that initially lacked that header were fixed by re-running the observation, not by editing the file: a capture that gets hand-edited stops being a capture.negative-cases/README.mddocuments the per-subdirectory convention and is deliberately not an index: the tree is shared across phases, and an enumeration would be stale on the next deposit — precisely the failure ADR-0016 exists to prevent.evidence/README.md,evidence/frozen-expectations/, andevidence/accept-corpus-freeze/are untouched and absent; they belong to the concurrent Phase B session.Verification
bun testbun run typecheckbun run check:depscore-has-no-adapter-deps: okbun run adr lintbun run build/bun run lintbun install --frozen-lockfileIncidental:
bun.lock's@adrkit/spec-kitversion moves0.1.0→0.1.2, correcting pre-existing staleness — that manifest already read0.1.2onmainand is unmodified here.Refs:
specs/010-catalog-backstage/tasks.mdT001–T012 · ADR-0007, ADR-0010, ADR-0013, ADR-0014, ADR-0016, ADR-0020 · Constitution v1.0.2 Principle III