Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions dist/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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 = [];
Expand Down
52 changes: 52 additions & 0 deletions docs/CONSUMER_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export function discoverUiContracts(root: string): UiContract[] {
const resolved = resolve(root);
const files = collectCandidateFiles(resolved);
const byId = new Map<string, UiContract>();
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 {
Expand All @@ -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));
Expand Down Expand Up @@ -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[] = [];
Expand Down
37 changes: 37 additions & 0 deletions tests/projectSetup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<aside id="review" data-nodekit-review-boundary="proposal"><button>Approve</button></aside>';
write(reviewPath, review + '<article id="artifact" data-nodekit-artifact-id="dynamic-123"></article>');
write(join(publicDir, "index.html"), '<button data-testid="request-proof">Request proof</button><div id="incidental"></div>');
write(join(dependency, "example.html"), '<button data-testid="dependency-example">Demo</button>');

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"), `<button data-testid="bounded-${index}">Review</button>`);
}
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(
Expand Down
Loading