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
72 changes: 67 additions & 5 deletions tools/substrate-claim-checker/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# substrate-claim-checker

V0 of the substrate-claim-checker per the verify-then-claim discipline
memo (`memory/feedback_verify_then_claim_discipline_dominant_failure_mode_substrate_authoring_otto_2026_05_03.md`).
Substrate-claim-checker per the verify-then-claim discipline memo
(`memory/feedback_verify_then_claim_discipline_dominant_failure_mode_substrate_authoring_otto_2026_05_03.md`).

Catches one class of drift today: **count drift** between narrative
claims (e.g. "18+ drift instances", "13-row table", "5 procedure
skills") and the actual count of structured rows the claims reference.
Catches two of the seven sub-classes B-0170 names:

- **Count drift** (v0.4.4) — between narrative claims (e.g. "18+ drift
instances", "13-row table", "5 procedure skills") and the actual
count of structured rows the claims reference. Implemented in
`check-counts.ts`.
- **Existence drift** (v0.5) — claims that a file or directory exists
when it doesn't on disk. Implemented in `check-existence.ts`.

The remaining 5 sub-classes (semantic-equivalence, empirical-output,
convention, path-form, self-recursive) are deferred to v0.6+.

## Usage

Expand Down Expand Up @@ -85,3 +93,57 @@ Per the verify-then-claim memo's mechanization-path section:

These will land in subsequent PRs once the tool's check-types are
mature enough to trust as gates.

## v0.5 — `check-existence.ts` (existence drift)

The `check-existence.ts` tool is the second sub-class checker, covering the **existence drift** sub-class (per the verify-then-claim memo's 7-class taxonomy).

Comment thread
AceHack marked this conversation as resolved.
### What it catches

Claims that a file or directory exists when it doesn't:

- Backtick-quoted paths: `` `path/to/X.md` ``
- Markdown link targets: `[text](relative/path)` — relative paths only
Comment thread
AceHack marked this conversation as resolved.

### Resolution order

For each path claim, tries three candidate roots:

1. The file's own directory (cross-references within the same dir)
2. The parent directory (bare-filename references for files in subdirs)
3. The repository root (repo-relative paths)

Stops on first hit. Emits a finding only if NO candidate root resolves.

### Future-state context detection

Claims marked future-state are exempt:

- `(proposed)`, `(planned)`, `(future)`, `(would be)`, `(not yet)`, `(tbd)`, `(deferred)`, `(pending)`
- Phrasings: "would be", "will be", "to be authored", "not yet exists", "doesn't yet exist", "future-state", "row deliverable", "I'm guessing", "concretely something like", "will probably", "lower confidence"

Detected within the line + ±1 line context window.

### Skipped automatically

- Glob patterns (`*`, `?`, `[...]`) — not real paths
- URLs (http://, https://, mailto:) — not file-system
- Anchor-only links (`#section`) — same-page anchors
- Absolute paths (`/etc/...`) — system paths, out of repo scope
- Short strings (<3 chars) — unlikely to be paths
- Placeholders (`<...>`, `{...}`, `XXX`)
- Fenced code blocks — example paths in code shouldn't false-positive

### Known limitations (v0.5)

- Calibration-delta tables that cite path-forms as discussion topics (not claims of existence) may false-positive. Mitigated by future-marker context detection but imperfect.
- Section-level future-state markers (e.g., a section header `## (Proposed) X`) don't propagate to claims further down. Use inline markers per claim or per paragraph.

### Usage

```
bun tools/substrate-claim-checker/check-existence.ts <file>
bun tools/substrate-claim-checker/check-existence.ts <file1> <file2> ...
```

Exit codes match check-counts.ts: `0` clean, `1` drift detected or input error.
253 changes: 253 additions & 0 deletions tools/substrate-claim-checker/check-existence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { findPathClaims, looksLikePath, isFutureStateContext, checkFile } from "./check-existence.ts";

Comment thread
AceHack marked this conversation as resolved.
Comment thread
AceHack marked this conversation as resolved.
describe("looksLikePath", () => {
test("recognizes relative paths", () => {
expect(looksLikePath("docs/research/foo.md")).toBe(true);
expect(looksLikePath("./tools/setup")).toBe(true);
expect(looksLikePath("../foo.md")).toBe(true);
});

test("rejects URLs and anchors", () => {
expect(looksLikePath("https://example.com")).toBe(false);
expect(looksLikePath("#section")).toBe(false);
expect(looksLikePath("mailto:x@y")).toBe(false);
});

test("rejects placeholders", () => {
expect(looksLikePath("path/<placeholder>/foo")).toBe(false);
expect(looksLikePath("foo/{name}/bar")).toBe(false);
expect(looksLikePath("XXX")).toBe(false); // bare placeholder
expect(looksLikePath("TBD")).toBe(false); // bare placeholder
});

test("accepts legitimate filenames containing placeholder words", () => {
// path/XXX or docs/TODO.md should NOT be rejected by the placeholder
// filter; only WHOLE-STRING placeholders are rejected. Reviewer-flagged
// false-positive class on PR #1298.
expect(looksLikePath("docs/TODO.md")).toBe(true);
expect(looksLikePath("notes/tbd-changes.md")).toBe(true);
});

test("rejects strings with spaces", () => {
expect(looksLikePath("foo bar")).toBe(false);
});

test("rejects absolute paths", () => {
expect(looksLikePath("/etc/hosts")).toBe(false);
});

test("rejects too-short", () => {
expect(looksLikePath("ab")).toBe(false);
});

test("recognizes file-with-extension only", () => {
expect(looksLikePath("foo.md")).toBe(true);
expect(looksLikePath("config.json")).toBe(true);
});
});

describe("isFutureStateContext", () => {
test("detects (proposed) marker", () => {
expect(isFutureStateContext("foo `bar.md` *(proposed)*", "", "")).toBe(true);
});

test("detects 'would be'", () => {
expect(isFutureStateContext("the file would be at `bar.md`", "", "")).toBe(true);
});

test("detects 'not yet exists'", () => {
expect(isFutureStateContext("`bar.md` not yet exists", "", "")).toBe(true);
});

test("rejects current-state claim", () => {
expect(isFutureStateContext("the file `bar.md` contains X", "", "")).toBe(false);
});

test("checks neighboring lines", () => {
expect(isFutureStateContext("`bar.md`", "(proposed)", "")).toBe(true);
});
});

describe("findPathClaims", () => {
test("finds backtick-quoted paths", () => {
const lines = ["See `docs/research/foo.md` for details."];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(1);
expect(claims[0]?.path).toBe("docs/research/foo.md");
expect(claims[0]?.line).toBe(1);
});

test("finds markdown link targets", () => {
const lines = ["See [the doc](docs/research/foo.md) for details."];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(1);
expect(claims[0]?.path).toBe("docs/research/foo.md");
});

test("skips fenced code blocks", () => {
const lines = [
"Real path: `docs/foo.md`",
"```bash",
"ls `path/inside/fence.md`",
"```",
"After: `docs/bar.md`",
];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(2);
expect(claims.map((c) => c.path).sort()).toEqual(["docs/bar.md", "docs/foo.md"]);
});

test("skips URLs in markdown links", () => {
const lines = ["[link](https://example.com/foo)"];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(0);
});

test("strips anchor from link target", () => {
const lines = ["[link](docs/foo.md#section)"];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(1);
expect(claims[0]?.path).toBe("docs/foo.md");
});
});

describe("checkFile", () => {
test("reports drift for a missing path claim", () => {
const dir = mkdtempSync(join(tmpdir(), "check-existence-"));
const tmpFile = join(dir, "test.md");
try {
writeFileSync(
tmpFile,
`# Test\n\nSee \`docs/this/path/does/not/exist/${Date.now()}.md\` for details.\n`,
);
const result = checkFile(tmpFile);
expect(result.ok).toBe(true);
expect(result.findings.length).toBeGreaterThan(0);
expect(result.findings[0]?.pathClaim).toContain("does/not/exist");
} finally {
try { unlinkSync(tmpFile); } catch {}
try { rmdirSync(dir); } catch {}
}
});

test("returns ok=false for missing input file", () => {
const dir = mkdtempSync(join(tmpdir(), "check-existence-"));
try {
const result = checkFile(join(dir, "nonexistent.md"));
expect(result.ok).toBe(false);
expect(result.findings).toEqual([]);
} finally {
try { rmdirSync(dir); } catch {}
}
});

test("returns ok=false for input that is a directory", () => {
const dir = mkdtempSync(join(tmpdir(), "check-existence-"));
try {
const result = checkFile(dir);
expect(result.ok).toBe(false);
expect(result.findings).toEqual([]);
} finally {
try { rmdirSync(dir); } catch {}
}
});

test("clean file produces no findings", () => {
const dir = mkdtempSync(join(tmpdir(), "check-existence-"));
const tmpFile = join(dir, "clean.md");
try {
writeFileSync(tmpFile, "# Test\n\nA simple memo with no path claims at all.\n");
const result = checkFile(tmpFile);
expect(result.ok).toBe(true);
expect(result.findings).toEqual([]);
} finally {
try { unlinkSync(tmpFile); } catch {}
try { rmdirSync(dir); } catch {}
}
});

test("future-state context exempts the claim", () => {
const dir = mkdtempSync(join(tmpdir(), "check-existence-"));
const tmpFile = join(dir, "future.md");
try {
writeFileSync(
tmpFile,
`# Test\n\nThe \`docs/proposed-path-${Date.now()}.md\` file would be created when this lands.\n`,
);
const result = checkFile(tmpFile);
expect(result.ok).toBe(true);
expect(result.findings).toEqual([]);
} finally {
try { unlinkSync(tmpFile); } catch {}
try { rmdirSync(dir); } catch {}
}
});
});

describe("looksLikePath - version-number rejection", () => {
test("rejects version numbers", () => {
expect(looksLikePath("v0.69.4")).toBe(false);
expect(looksLikePath("10.0.203")).toBe(false);
expect(looksLikePath("1.2.3")).toBe(false);
expect(looksLikePath("1.2.3-rc1")).toBe(false);
});

test("accepts known-extension paths even without slash", () => {
expect(looksLikePath("config.toml")).toBe(true);
expect(looksLikePath("README.md")).toBe(true);
expect(looksLikePath("script.sh")).toBe(true);
});

test("rejects unknown-extension single-component", () => {
expect(looksLikePath("foo.zzzz")).toBe(false);
expect(looksLikePath("file.unknownext")).toBe(false);
});
});

describe("looksLikePath - cross-platform absolute paths", () => {
test("rejects POSIX absolute", () => {
expect(looksLikePath("/etc/hosts")).toBe(false);
expect(looksLikePath("/usr/local/bin/foo")).toBe(false);
});

test("rejects Windows drive paths even on POSIX", () => {
// path.isAbsolute returns false for these on POSIX, so we need
// explicit regex checks.
expect(looksLikePath("C:\\Windows\\System32\\foo.dll")).toBe(false);
expect(looksLikePath("D:/Users/foo/bar.txt")).toBe(false);
expect(looksLikePath("c:/lower/case/drive.md")).toBe(false);
});

test("rejects Windows UNC paths", () => {
expect(looksLikePath("\\\\server\\share\\foo.md")).toBe(false);
});
});

describe("findPathClaims - angle-bracket link targets", () => {
test("strips angle-brackets from link target", () => {
const lines = ["See [spec](<docs/foo.md>) for details."];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(1);
expect(claims[0]?.path).toBe("docs/foo.md");
});

test("handles angle-brackets with anchors", () => {
const lines = ["See [spec](<docs/foo.md#section>)"];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(1);
expect(claims[0]?.path).toBe("docs/foo.md");
});

test("regular links still work alongside angle-bracket variant", () => {
const lines = [
"[a](docs/normal.md) and [b](<docs/angled.md>)",
];
const claims = findPathClaims(lines);
expect(claims).toHaveLength(2);
expect(claims.map((c) => c.path).sort()).toEqual(["docs/angled.md", "docs/normal.md"]);
});
});
Loading
Loading