From bac8f7ee1c0fa76493204bb845f5cc9b6be7dcee Mon Sep 17 00:00:00 2001 From: homen Date: Fri, 4 Sep 2026 20:08:27 -0700 Subject: [PATCH] fix: discover explicit review boundaries in generated consumers --- README.md | 2 ++ dist/project.js | 6 ++--- docs/CONSUMER_HANDOFF.md | 52 ++++++++++++++++++++++++++++++++++++++ src/project.ts | 6 ++--- tests/projectSetup.test.ts | 37 +++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 docs/CONSUMER_HANDOFF.md diff --git a/README.md b/README.md index 559fe24..b5cf5b1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ cloning it, read every `node dist/cli.js` below as `npx proofloop`. Zero runtime dependencies. Node >= 20. Works on any repo. +For an existing application, read the [consumer handoff](docs/CONSUMER_HANDOFF.md): exact local-package installation, preserved agent ownership, real gate failure/recovery, and the boundary between setup reports and proof. + > **New to this codebase?** Read [`docs/START_HERE.md`](docs/START_HERE.md) > instead of this file. It walks one real user action through the code in the > order it executes, naming every file and line. Then open `.tours/` in VS Code diff --git a/dist/project.js b/dist/project.js index af46a30..228d2b9 100644 --- a/dist/project.js +++ b/dist/project.js @@ -310,7 +310,7 @@ function discoverUiContracts(root) { const resolved = (0, node_path_1.resolve)(root); const files = collectCandidateFiles(resolved); const byId = new Map(); - const attrPattern = /data-(testid|proofloop)\s*=\s*["'`]([^"'`]+)["'`]/g; + const attrPattern = /data-(testid|proofloop|nodekit-review-boundary)\s*=\s*["'`]([^"'`]+)["'`]/g; for (const file of files) { let text = ""; try { @@ -320,7 +320,7 @@ function discoverUiContracts(root) { continue; } for (const match of text.matchAll(attrPattern)) { - const attr = match[1] === "proofloop" ? "data-proofloop" : "data-testid"; + const attr = `data-${match[1]}`; const id = match[2].trim(); if (!id || byId.has(id)) continue; @@ -684,7 +684,7 @@ function hasProofloopGithubWorkflow(root) { return false; } function collectCandidateFiles(root) { - const roots = ["src", "app", "pages", "components", "e2e", "tests", "test", "proofloop"] + const roots = ["src", "app", "apps", "public", "pages", "components", "e2e", "tests", "test", "proofloop"] .map((name) => (0, node_path_1.join)(root, name)) .filter((path) => (0, node_fs_1.existsSync)(path)); const files = []; diff --git a/docs/CONSUMER_HANDOFF.md b/docs/CONSUMER_HANDOFF.md new file mode 100644 index 0000000..58c0a67 --- /dev/null +++ b/docs/CONSUMER_HANDOFF.md @@ -0,0 +1,52 @@ +# Adopt NodeProof into a real application + +A developer handing an application to another person needs a failed check to remain visible after the original session ends. Install the exact reviewed NodeProof package into the application, define its real checks, and retain the gate receipt. Running doctor inside the NodeProof library checkout does not initialize that other application's manifest or agent instructions. + +## Keep source identity and ownership explicit + +Record the NodeProof source commit or reviewed candidate digest and the SHA-256 of its packed tarball. Copy that tarball into the consumer's `vendor` directory, install it by file path, and retain the resulting lockfile. The local installed CLI is the executable for this consumer; a registry package with the same version number is not source identity. + +```powershell +npm install --save-dev ./vendor/proofloop.tgz +node node_modules/proofloop/dist/cli.js init +``` + +Omitting `--agent` and `--live` preserves an existing application's agent documents and avoids adding unrelated live templates. `init` still writes its manifest and `proofloop:*` package aliases. It preserves an existing gate config, but regenerates those aliases and the manifest. Use the explicit installed path above when exact package identity matters. Existing non-ProofLoop package scripts remain owned by the application. + +## The NodeKit generated-consumer example + +The verified integration uses NodeKit source `53348d606b2dfe4fc0bfb4b7d51291f7892afeb0`, its deterministic case workflow and Node 22. It does not claim a later NodeKit revision. Keep its AGENTS.md, CLAUDE.md and native host skill trees unchanged. Configure these actual checks in `proofloop.config.json`: + +```json +{ + "app": "NodeKit generated case workflow", + "workflow": "Prove compiled contracts, application scenarios, proposal conflicts and artifact receipts before handoff.", + "gate": { "checks": [ + { "name": "compiled contracts and application scenarios", "command": "npm run check" }, + { "name": "proposal conflict and recovery evaluation", "command": "npm run eval" }, + { "name": "canonical artifact receipt proof", "command": "npm run proof" } + ] }, + "immutable": ["scripts/check.mjs", "scripts/eval.mjs", "scripts/proof.mjs", "test/workflow.test.mjs", "test/outcome.test.mjs"], + "protectedPaths": [] +} +``` + +On a fresh generated consumer, run `npm ci`, `npm run compile`, then **`npm run demo` before the gate**. This NodeKit version needs the demo to create its proof directory and demo receipt. Its check can otherwise pass all nine application tests and still exit nonzero while writing the missing proof directory. Keep that failure visible; it is not a passing gate. Generate compiled output deliberately before verification; the gate's existing check uses `compile --check`, so stale generated configuration cannot silently repair itself inside the gate. + +```powershell +node node_modules/proofloop/dist/cli.js gate +node node_modules/proofloop/dist/cli.js gate --check +node node_modules/proofloop/dist/cli.js resume --json +node node_modules/proofloop/dist/cli.js report latest --json +node node_modules/proofloop/dist/cli.js doctor --json +``` + +The gate exits 0 for pass, 1 for failure and 2 when no gate/result exists. `--check` reads the last receipt without running commands; after changing application files, run the real gate again. Preserve earlier `.proofloop/gate-state.json` snapshots separately before another run replaces that latest receipt. A new process must report the same failing check; restoring the exact application/generated bytes and rerunning the gate must recover without weakening its checks. + +## What discovery and doctor actually establish + +Discovery includes `apps` and `public` alongside the existing source roots. It recognizes explicit `data-testid`, `data-proofloop` and `data-nodekit-review-boundary` markers. The NodeKit app already declares and tests `[data-nodekit-review-boundary="proposal"]` in `apps/web/public/index.html`; no placeholder DOM marker is needed. Ordinary IDs and dynamic `data-nodekit-artifact-*` values are not promoted into contracts. The existing scan is bounded to 800 source files and excludes dependency/build directories; very large workspaces may need a separate scoped review. + +`doctor` always exits zero because it reports setup. Read `ready` and `missing`; declared browser dependencies, discovered selectors and a CI file are configuration evidence, not a rendered-browser pass, a CI run or host login. The consumer's proposal review and approval were observed in a browser separately. Whole application and NodeProof public-site UI grades remain separate work. + +If generating a local CI handoff, preserve the application's Node requirement (Node 22 in this example) and run `node node_modules/proofloop/dist/cli.js` after the locked install. The stock template's Node 20 and registry-qualified command are not this consumer's exact environment. A local workflow file is unexecuted until a real CI run proves it. Hook installation, coding-host activation, providers and deployment are separate actions; this adoption does not activate them. diff --git a/src/project.ts b/src/project.ts index 227fe44..69f092a 100644 --- a/src/project.ts +++ b/src/project.ts @@ -345,7 +345,7 @@ export function discoverUiContracts(root: string): UiContract[] { const resolved = resolve(root); const files = collectCandidateFiles(resolved); const byId = new Map(); - const attrPattern = /data-(testid|proofloop)\s*=\s*["'`]([^"'`]+)["'`]/g; + const attrPattern = /data-(testid|proofloop|nodekit-review-boundary)\s*=\s*["'`]([^"'`]+)["'`]/g; for (const file of files) { let text = ""; try { @@ -354,7 +354,7 @@ export function discoverUiContracts(root: string): UiContract[] { continue; } for (const match of text.matchAll(attrPattern)) { - const attr = match[1] === "proofloop" ? "data-proofloop" : "data-testid"; + const attr = `data-${match[1]}`; const id = match[2].trim(); if (!id || byId.has(id)) continue; const windowText = text.slice(Math.max(0, match.index - 800), Math.min(text.length, match.index + 1200)); @@ -704,7 +704,7 @@ function hasProofloopGithubWorkflow(root: string): boolean { } function collectCandidateFiles(root: string): string[] { - const roots = ["src", "app", "pages", "components", "e2e", "tests", "test", "proofloop"] + const roots = ["src", "app", "apps", "public", "pages", "components", "e2e", "tests", "test", "proofloop"] .map((name) => join(root, name)) .filter((path) => existsSync(path)); const files: string[] = []; diff --git a/tests/projectSetup.test.ts b/tests/projectSetup.test.ts index e464cb4..c97ecec 100644 --- a/tests/projectSetup.test.ts +++ b/tests/projectSetup.test.ts @@ -29,6 +29,43 @@ function write(path: string, content: string): void { } describe("agent-friendly project setup", () => { + it("a monorepo developer can rediscover explicit review contracts without trusting incidental markup or stale files", () => { + const root = tempRoot(); + const app = join(root, "apps", "web", "public"); + const publicDir = join(root, "public"); + const dependency = join(root, "apps", "web", "node_modules", "widget"); + for (const dir of [app, publicDir, dependency]) mkdirSync(dir, { recursive: true }); + const reviewPath = join(app, "index.html"); + // The NodeKit consumer already declares and verifies this proposal boundary. + const review = ''; + write(reviewPath, review + '
'); + write(join(publicDir, "index.html"), '
'); + write(join(dependency, "example.html"), ''); + + const expected = discoverUiContracts(root); + expect(expected.map((contract) => contract.id)).toEqual(["proposal", "request-proof"]); + expect(expected.find((contract) => contract.id === "proposal")).toMatchObject({ + selector: '[data-nodekit-review-boundary="proposal"]', + source: "apps/web/public/index.html", + }); + expect(expected.find((contract) => contract.id === "request-proof")?.source).toBe("public/index.html"); + for (let scan = 0; scan < 5; scan++) expect(discoverUiContracts(root)).toEqual(expected); + rmSync(reviewPath); + expect(discoverUiContracts(root).map((contract) => contract.id)).toEqual(["request-proof"]); + write(reviewPath, review); + expect(discoverUiContracts(root)).toEqual(expected); + + // A large workspace still respects the existing 800-source-file scan budget. + const bulk = join(root, "apps", "bulk"); + mkdirSync(bulk); + for (let index = 0; index < 805; index++) { + write(join(bulk, String(index).padStart(4, "0") + ".html"), ``); + } + const bounded = discoverUiContracts(root); + expect(bounded).toHaveLength(800); + expect(bounded.every((contract) => contract.source.startsWith("apps/bulk/"))).toBe(true); + }); + it("init --agent all --live writes docs, manifest, package aliases, and live scaffold", () => { const root = tempRoot(); write(