diff --git a/AGENTS.md b/AGENTS.md index 45fbb68..7cbaebb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - `src/next-config.ts` owns production-only source-map configuration. - `src/*.test.ts` and `src/*.property.test.ts` hold deterministic regressions and general laws. - `scripts/` builds isolated exports and verifies package, bundle, and public boundaries. +- `portfolio-inventory.json` records the package publication boundary in the shared fleet contract. - `.github/workflows/` runs read-only continuous integration and publishes only a verified immutable release. - `.agents/skills/` contains the seven portable knowledge and phased-execution workflows. - `kb/` contains authored repository rationale, maintained synthesis, and implementation plans. @@ -25,6 +26,7 @@ - Treat source-map credentials as build-time secrets. Upload only for an exact production deployment with a supported PostHog UI host and a commit release identifier. - Keep Direct deterministic compositions development-only and outside every production dependency graph and published export. - Freeze package interfaces before parallel lanes begin. Give exports, manifests, lockfiles, generated output, and other convergence surfaces one owner while lanes edit disjoint paths. +- Keep `portfolio-inventory.json` generated from `package.json`; the checked inventory must match the public package name, version, repository, and Hraness dependency edges exactly. - Keep mandatory rules in the closest `AGENTS.md`, executable contracts in types and tests, and pull-based rationale, evidence, synthesis, and plans in `kb/`. - Use Bun 1.3.14 for installs, builds, and tests. Verify every installed export with genuine Node 24. - Run `bun run check` before release handoff. The release workflow may write only the verified immutable GitHub Release. diff --git a/package.json b/package.json index fa50bdf..82b9396 100644 --- a/package.json +++ b/package.json @@ -78,13 +78,14 @@ "kb:catalog": "bunx --bun github:hraness/kb#v0.15.1 catalog --root kb", "build": "bun run ./scripts/build.ts", "check:boundaries": "bun run ./scripts/check-bundle-boundaries.ts", + "check:portfolio-inventory": "bun run ./scripts/check-portfolio-inventory.ts", "check:public-boundary": "bun run ./scripts/check-public-boundary.ts", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "bun test ./src", "test:property": "bun test ./src/*.property.test.ts", "test:package": "bun run ./scripts/package-smoke.ts", - "check": "bun run check:public-boundary && bun run lint && bun run typecheck && bun run build && bun run check:boundaries && bun run test && bun run test:property && bun run test:package && bun run kb:check", + "check": "bun run check:portfolio-inventory && bun run check:public-boundary && bun run lint && bun run typecheck && bun run build && bun run check:boundaries && bun run test && bun run test:property && bun run test:package && bun run kb:check", "prepack": "bun run check" }, "dependencies": { diff --git a/portfolio-inventory.json b/portfolio-inventory.json new file mode 100644 index 0000000..02a475a --- /dev/null +++ b/portfolio-inventory.json @@ -0,0 +1,24 @@ +{ + "contract": "hraness.portfolio-inventory/v1", + "formatVersion": 1, + "repository": "hraness/posthog", + "components": [ + { + "kind": "package", + "name": "@hraness/posthog", + "path": ".", + "visibility": "public", + "version": "0.1.0" + } + ], + "dependencies": [], + "deployments": [], + "brands": [], + "publications": [ + { + "component": "@hraness/posthog", + "packageName": "@hraness/posthog", + "repository": "hraness/posthog" + } + ] +} diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index f12440e..605f6dd 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -4,10 +4,12 @@ - `check-bundle-boundaries.ts` proves runtime dependency isolation between pure, browser, React, Node, and build-time exports. - `check-public-boundary.ts` rejects private provenance and credential-like values. - `package-smoke.ts` installs the packed artifact, imports every built export with Node 24, and checks source types from a consumer. +- `check-portfolio-inventory.ts` derives and verifies the canonical public package inventory. # Guidelines - Keep verification deterministic, cross-platform where practical, and free of network writes beyond dependency installation. +- Derive fleet inventory from package metadata; never maintain a second hand-authored dependency graph. - Verify the packed artifact rather than relying only on source imports. - Update bundle allowlists only when an intentional public dependency boundary changes. - Keep release mutation in the release workflow. Ordinary checks remain read-only. diff --git a/scripts/check-portfolio-inventory.ts b/scripts/check-portfolio-inventory.ts new file mode 100644 index 0000000..9fdba9a --- /dev/null +++ b/scripts/check-portfolio-inventory.ts @@ -0,0 +1,103 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +type DependencyScope = "development" | "optional" | "peer" | "runtime"; + +function record(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } + return value as Record; +} + +function stringField(value: Record, key: string): string { + const field = value[key]; + if (typeof field !== "string" || field.length === 0) { + throw new Error(`package.json ${key} must be a non-empty string`); + } + return field; +} + +function repositorySlug(value: unknown): string { + const repository = record(value, "package.json repository"); + const url = stringField(repository, "url"); + const match = /github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/u.exec(url); + if (match?.[1] === undefined) { + throw new Error( + "package.json repository.url must identify a GitHub repository", + ); + } + return match[1]; +} + +function asciiCompare(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + +const repositoryRoot = resolve(import.meta.dir, ".."); +const packageManifest = record( + JSON.parse( + await readFile(resolve(repositoryRoot, "package.json"), "utf8"), + ) as unknown, + "package.json", +); +const packageName = stringField(packageManifest, "name"); +const version = stringField(packageManifest, "version"); +const repository = repositorySlug(packageManifest.repository); +const dependencySections = [ + ["devDependencies", "development"], + ["optionalDependencies", "optional"], + ["peerDependencies", "peer"], + ["dependencies", "runtime"], +] as const satisfies readonly (readonly [string, DependencyScope])[]; +const dependencies = dependencySections.flatMap(([section, scope]) => { + const value = packageManifest[section]; + if (value === undefined) return []; + const entries = record(value, `package.json ${section}`); + return Object.entries(entries) + .filter(([name]) => name.startsWith("@hraness/")) + .map(([name, specifier]) => { + if (typeof specifier !== "string" || specifier.length === 0) { + throw new Error( + `package.json ${section}.${name} must be a non-empty string`, + ); + } + return { from: packageName, scope, specifier, to: name }; + }); +}).toSorted((left, right) => + asciiCompare(left.from, right.from) + || asciiCompare(left.to, right.to) + || asciiCompare(left.scope, right.scope) + || asciiCompare(left.specifier, right.specifier)); + +const expected = { + contract: "hraness.portfolio-inventory/v1", + formatVersion: 1, + repository, + components: [{ + kind: "package", + name: packageName, + path: ".", + visibility: "public", + version, + }], + dependencies, + deployments: [], + brands: [], + publications: [{ + component: packageName, + packageName, + repository, + }], +}; +const expectedBytes = `${JSON.stringify(expected, null, 2)}\n`; +const actualBytes = await readFile( + resolve(repositoryRoot, "portfolio-inventory.json"), + "utf8", +); + +if (actualBytes !== expectedBytes) { + throw new Error( + "portfolio-inventory.json does not match the canonical package inventory", + ); +}