From ef0541ccc8bfc845049a7d6507caf6b27134a065 Mon Sep 17 00:00:00 2001 From: verlyn13 Date: Sat, 25 Jul 2026 13:07:57 -0600 Subject: [PATCH] feat(kernel): register the @hcs/kernel workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First non-.gitkeep file in Ring 1. packages/kernel has held an empty marker since the repo was scaffolded. Three files: a package manifest, an empty public-API barrel, and a test suite that guards the ring boundary. THE EXPORTS MAP IS THE POINT charter §Package boundary enforcement: "packages/adapters/** cannot import from packages/kernel/src/** except through the declared public API surface (packages/kernel/src/api/)." That rule now has two independent enforcers: 1. scripts/ci/boundary-check.sh rule 2 — a grep. It went three months without executing once: a PCRE lookahead under `grep -E` exited rc=2 and `2>/dev/null` ate the diagnostic, so it printed "ring boundaries intact" while a planted violation passed. Repaired in #93. 2. This package's `exports` map, which publishes `.` and `./api` and nothing else. The second is the durable one. A grep can regress silently; a missing exports entry cannot. Verified on this host: $ node -e "import('@hcs/kernel/api')" RESOLVED, exports: [] $ node -e "import('@hcs/kernel/src/policy/rule-resolution')" BLOCKED by exports map: ERR_PACKAGE_PATH_NOT_EXPORTED Node refuses the deep import before any CI gate has an opinion. The test suite asserts the map publishes exactly two entry points, routes both through src/api/, and contains no wildcard or `src` path — so a future PR cannot widen it to make a convenient import work without turning the suite red. WHY THE BARREL IS EMPTY Deliberate. This is the workspace scaffold, and it is separated from the first service so that workspace wiring problems surface in a three-file diff rather than inside a six-hundred-line one. The first service to land here is the read-only policy-snapshot loader, assigned by ADR 0060 §Ring-1 policy/gateway loader, with its checkpoint-level test obligation specified by ADR 0061 — reject at the digest-verification step, not merely at the final Decision. Both are accepted; no new ADR is required. NO JUSTFILE EDIT WAS NEEDED `just test kernel` works the moment this package has a tests/ directory, because #93 replaced the hardcoded `case "$target" in ""|schemas)` with discovery over packages/*/tests. Confirmed: $ just test kernel ✓ packages/kernel/tests/api-surface.test.ts (5 tests) Test Files 1 passed (1) That was the stated payoff of the discovery change and it holds. It also means this PR adds zero merge-collision surface on the justfile. Class D — kernel, read path. Registers no capability, exposes no agent-callable surface, emits no OperationShape, mints and consumes no ApprovalGrant, adds no dependency. Validation: `just verify` green. `just test kernel` 5/5. --- package-lock.json | 8 +++ packages/kernel/.gitkeep | 0 packages/kernel/package.json | 10 ++++ packages/kernel/src/api/index.ts | 36 ++++++++++++++ packages/kernel/tests/api-surface.test.ts | 60 +++++++++++++++++++++++ 5 files changed, 114 insertions(+) delete mode 100644 packages/kernel/.gitkeep create mode 100644 packages/kernel/package.json create mode 100644 packages/kernel/src/api/index.ts create mode 100644 packages/kernel/tests/api-surface.test.ts diff --git a/package-lock.json b/package-lock.json index 82a61af..c5d288e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,6 +200,10 @@ "node": ">=14.21.3" } }, + "node_modules/@hcs/kernel": { + "resolved": "packages/kernel", + "link": true + }, "node_modules/@hcs/schemas": { "resolved": "packages/schemas", "link": true @@ -1689,6 +1693,10 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "packages/kernel": { + "name": "@hcs/kernel", + "version": "0.1.0" + }, "packages/schemas": { "name": "@hcs/schemas", "version": "0.1.0" diff --git a/packages/kernel/.gitkeep b/packages/kernel/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/kernel/package.json b/packages/kernel/package.json new file mode 100644 index 0000000..618e5b9 --- /dev/null +++ b/packages/kernel/package.json @@ -0,0 +1,10 @@ +{ + "name": "@hcs/kernel", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/api/index.ts", + "./api": "./src/api/index.ts" + } +} diff --git a/packages/kernel/src/api/index.ts b/packages/kernel/src/api/index.ts new file mode 100644 index 0000000..0a72ce0 --- /dev/null +++ b/packages/kernel/src/api/index.ts @@ -0,0 +1,36 @@ +/** + * @hcs/kernel — Ring 1 public API surface. + * + * THIS FILE IS THE ONLY LEGAL IMPORT PATH FOR RING 2. + * + * Per the implementation charter §Package boundary enforcement: + * + * "packages/adapters/** cannot import from packages/kernel/src/** except + * through the declared public API surface (packages/kernel/src/api/)." + * + * That rule is enforced twice, deliberately: + * + * 1. `scripts/ci/boundary-check.sh` rule 2 greps for deep kernel imports in + * Ring 2 and subtracts the `/api/` path. That check went three months + * without executing once — it used a PCRE lookahead under `grep -E`, + * exited rc=2, and the error was silenced. Repaired in PR #93. + * 2. This package's `exports` map. It publishes `.` and `./api` and nothing + * else, so `import "@hcs/kernel/src/policy/x"` is unresolvable at + * runtime — Node refuses it before any CI gate has an opinion. + * + * The second mechanism is the durable one. A grep can regress silently; a + * missing exports entry cannot. Do not add a `"./src/*"` or wildcard entry to + * the exports map to make an import work — that would convert a hard boundary + * back into a linted one. `packages/kernel/tests/api-surface.test.ts` asserts + * this and will fail if it happens. + * + * Ring 1 imports Ring 0 (`@hcs/schemas`) and nothing above it. + * + * Empty by design at this commit: this is the workspace scaffold. The first + * service to land here is the read-only policy-snapshot loader, assigned by + * ADR 0060 §Ring-1 policy/gateway loader and with its checkpoint-level test + * obligation specified by ADR 0061 (reject at the digest-verification step, + * not merely at the final Decision). + */ + +export {}; diff --git a/packages/kernel/tests/api-surface.test.ts b/packages/kernel/tests/api-surface.test.ts new file mode 100644 index 0000000..3e8fabf --- /dev/null +++ b/packages/kernel/tests/api-surface.test.ts @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +/** + * The ring boundary between Ring 2 (adapters, dashboard) and Ring 1 (kernel) is + * enforced by two independent mechanisms. This suite guards the one that cannot + * regress silently. + * + * `scripts/ci/boundary-check.sh` rule 2 is a grep. Greps fail open: that exact + * rule used a PCRE lookahead under `grep -E`, exited rc=2, had its diagnostic + * silenced by `2>/dev/null`, and reported "ring boundaries intact" for three + * months without ever executing. It was repaired in PR #93, but the class of + * failure is inherent to grep-based enforcement. + * + * The `exports` map is not a grep. If `@hcs/kernel/src/policy/x` is not + * published, Node refuses to resolve it — no CI gate needs an opinion. These + * assertions exist so that a future PR cannot quietly widen the map to make a + * convenient deep import work. + */ + +const manifestPath = fileURLToPath(new URL('../package.json', import.meta.url)); +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { + name: string; + type: string; + exports: Record; +}; + +describe('@hcs/kernel public API surface', () => { + it('publishes exactly the two documented entry points', () => { + expect(Object.keys(manifest.exports).sort()).toEqual(['.', './api']); + }); + + it('routes every entry point through src/api/', () => { + for (const target of Object.values(manifest.exports)) { + expect(target).toBe('./src/api/index.ts'); + } + }); + + it('publishes no wildcard or deep-path export', () => { + // A "./src/*" or "./*" entry would make every kernel internal importable + // from Ring 2, downgrading a hard boundary to a linted one. + for (const specifier of Object.keys(manifest.exports)) { + expect(specifier).not.toContain('*'); + expect(specifier).not.toContain('src'); + } + }); + + it('is an ESM package named @hcs/kernel', () => { + expect(manifest.name).toBe('@hcs/kernel'); + expect(manifest.type).toBe('module'); + }); + + it('exposes no runtime symbols yet — the scaffold is deliberately empty', async () => { + const api = await import('../src/api/index.ts'); + // Ring-1 services land here one at a time, each with its own ADR. The first + // is the read-only policy-snapshot loader (ADR 0060 / ADR 0061). + expect(Object.keys(api)).toEqual([]); + }); +});