diff --git a/apps/landing-page-astro/src/data/unpack-reports.json b/apps/landing-page-astro/src/data/unpack-reports.json index fbbc6c06..e754cf5b 100644 --- a/apps/landing-page-astro/src/data/unpack-reports.json +++ b/apps/landing-page-astro/src/data/unpack-reports.json @@ -91314,64 +91314,6 @@ "agent_handoff": { "summary": "Agents should treat this as a layered contract compiler plus a runtime, not as a Next.js app. Start from Agents.md, the architecture overview, and architecture.config.json. Use pnpm on Node 24, rebuild before typecheck, and do not bypass lint:deps. Tests are written first and run with Vitest; name the Contract type at every JSON boundary; never use any, zod, or a bare cast. Skills and rules have one canonical directory each. The traps that waste a session are stale dist types, plane violations, fixture drift, warn-only marker checks, and editing a gitignored symlink tree.", "claims": [ - { - "claim": "Agents.md requires Node from the shell (engines.node in the root package.json; do not run nvm/fnm), pnpm instead of npm, and never npx. Build with pnpm build. Use the repo scripts pnpm typecheck and pnpm test rather than hand-rolled tsc or vitest. Use arktype, not zod. Never add extensions to TypeScript imports.", - "sources": [ - "Agents.md", - "package.json#L6-L62", - "package.json#L101-L103" - ], - "kind": "evidence" - }, - { - "claim": "Type rules from Agents.md: never any; never @ts-expect-error outside negative type tests; never @ts-nocheck; never suppress biome lints; no bare as in production code. Use blindCast or castAs from @internal/utils/casts. as const and test files are exempt. pnpm lint:casts is the ratchet.", - "sources": [ - "Agents.md", - "package.json#L36" - ], - "kind": "evidence" - }, - { - "claim": "Write tests before changing implementation. Omit the word should in test names. In sql-orm-client tests, assert the whole result shape with toEqual or a snapshot and an explicit select. Do not reexport from one file to another except under exports/ folders. Do not add backwards-compat exports unless asked. Do not branch on target; add an adapter.", - "sources": [ - "Agents.md" - ], - "kind": "evidence" - }, - { - "claim": "Where JSON enters the runtime, name the contract.d.ts type: postgres({ contractJson, url }) or validateSqlContractFully(contractJson). Do not use a wide generic such as SqlContract. The postgres() overloads encode that split between a live contract object and contractJson.", - "sources": [ - "Agents.md", - "packages/3-extensions/postgres/src/runtime/postgres.ts#L155-L170" - ], - "kind": "evidence" - }, - { - "claim": "After changing exported types in a package other packages consume, rebuild that package so dist/*.d.mts updates before downstream typecheck. Turbo already makes typecheck depend on build and ^build because self-imports of published subpaths read dist.", - "sources": [ - "Agents.md", - "turbo.json#L35-L43" - ], - "kind": "evidence" - }, - { - "claim": "Safe command set for a small change: pnpm build for the touched package, pnpm test:packages or that package's vitest, pnpm typecheck:packages, and pnpm lint:deps if imports moved. Contract artifact edits need pnpm fixtures:check rather than a hand-rolled emit-and-diff. Integration behavior needs pnpm test:integration; CLI journeys need pnpm test:journeys.", - "sources": [ - "package.json#L14-L23", - "package.json#L34", - "package.json#L57-L60", - "Agents.md" - ], - "kind": "evidence" - }, - { - "claim": "Edit skills at skills-contrib//SKILL.md and rules at .agents/rules/.mdc. .claude, .cursor, and .agents skill/rule trees are presentation symlinks materialized by prepare (skills add and scripts/sync-agent-rules.mjs). A rule added only under .cursor/rules is gitignored and lost. pnpm rules:sync and pnpm lint:rules:symlinks check the trees.", - "sources": [ - "Agents.md", - "package.json#L42-L69" - ], - "kind": "evidence" - }, { "claim": "New packages must be registered in architecture.config.json with plane-specific globs, broad to specific, including a migration glob for src/core/migrations/** even if the directory is empty. pnpm lint:deps fails the PR if a migration file imports a runtime package.", "sources": [ @@ -108906,14 +108848,6 @@ ], "kind": "evidence" }, - { - "claim": "libs/hbb_common is a git submodule (url https://github.com/rustdesk/hbb_common) that supplies rendezvous_proto, message framing, socket_client, Config/LocalConfig/PeerConfig, tokio re-exports and sodiumoxide crypto; in this clone the directory is empty, so no code under it can be read or built from this checkout.", - "sources": [ - ".gitmodules", - "libs/hbb_common" - ], - "kind": "evidence" - }, { "claim": "crates-io is patched: `libxdo-sys` is replaced by libs/libxdo-sys-stub (so builds work without libxdo on Wayland-only systems), and `tungstenite`, `webrtc`, `webrtc-util`, `webrtc-sctp` are pinned by git rev to rustdesk-org forks carrying custom fixes (incremental ws read buffer, Windows IPv6 ICE, SCTP congestion-control off by default).", "sources": [ @@ -109491,14 +109425,6 @@ ], "kind": "evidence" }, - { - "claim": "libs/hbb_common is empty in this clone — all wire protocol, Config, socket_client, and sodiumoxide usage is invisible to audit here; any build/test requires `git submodule update --init` first.", - "sources": [ - ".gitmodules", - "libs/hbb_common" - ], - "kind": "evidence" - }, { "claim": "Forked network stack risk: webrtc/webrtc-sctp/webrtc-util and tungstenite are pinned to rustdesk-org forks with custom congestion-control and parsing changes; upstream CVE fixes will not flow through cargo update and must be cherry-picked by rev.", "sources": [ @@ -109635,15 +109561,6 @@ ], "kind": "evidence" }, - { - "claim": "Bootstrap trap: `git submodule update --init libs/hbb_common` is mandatory — the directory is empty in this clone and every protocol symbol (RendezvousMessage, Config, Stream, socket_client, tokio re-export) resolves from it; build also needs flutter_rust_bridge codegen for `bridge_generated.rs` when the `flutter` feature is on.", - "sources": [ - ".gitmodules", - "libs/hbb_common", - "src/lib.rs#L34-39" - ], - "kind": "evidence" - }, { "claim": "Toolchain pins: rust-version 1.75 (Cargo.toml), CI RUST_VERSION 1.75 / macOS 1.81, Flutter 3.24.5 (3.44.9 win-arm64), flutter_rust_bridge '=1.80'/1.80.1 — mismatched frb versions regenerate incompatible bindings.", "sources": [ @@ -125817,15 +125734,6 @@ ], "kind": "evidence" }, - { - "claim": "Renderers are pluggable behind renderer/base's IRenderer: AtlasEngine (D2D/D3D, custom shaders) for the new terminal, a GDI engine under renderer/gdi for legacy conhost, plus renderer/uia and renderer/wddmcon variants.", - "sources": [ - "src/renderer/atlas/AtlasEngine.cpp", - "src/renderer/base/Renderer.cpp", - "src/renderer/gdi" - ], - "kind": "evidence" - }, { "claim": "Settings layering is tag-based: ParsedSettings entries carry an OriginTag (InBox, User, Generated, Dynamic, Fragment) and SettingsLoader merges them in a fixed order — new settings sources plug in as additional origins without changing the merge engine.", "sources": [ diff --git a/apps/landing-page-astro/src/pages/unpack/[slug].astro b/apps/landing-page-astro/src/pages/unpack/[slug].astro index 32a738ab..28b370cc 100644 --- a/apps/landing-page-astro/src/pages/unpack/[slug].astro +++ b/apps/landing-page-astro/src/pages/unpack/[slug].astro @@ -25,7 +25,9 @@ const { report } = Astro.props;

Scanned commit {report.commit.sha.slice(0, 10)} dated {report.commit.date} — last upstream commit. Generated by {report.codevetterVersion} on - {' '}{new Date(report.collectedAt).toISOString().slice(0, 10)}. + {' '}{Number.isNaN(Date.parse(report.collectedAt)) + ? report.collectedAt + : new Date(report.collectedAt).toISOString().slice(0, 10)}.

github.com/{report.repo} ↗ @@ -84,7 +86,13 @@ const { report } = Astro.props; {c.kind === 'inference' && (inferred)}

{(c.sources ?? []).map((s) => ( - + {s} ))} diff --git a/apps/landing-page-astro/src/pages/unpack/[slug]/[section].astro b/apps/landing-page-astro/src/pages/unpack/[slug]/[section].astro index 7437186e..6fed3000 100644 --- a/apps/landing-page-astro/src/pages/unpack/[slug]/[section].astro +++ b/apps/landing-page-astro/src/pages/unpack/[slug]/[section].astro @@ -25,6 +25,7 @@ const { report, section, sectionKey, title } = Astro.props;
← {report.repo} @@ -54,7 +55,13 @@ const { report, section, sectionKey, title } = Astro.props; {c.kind === 'inference' && (inferred)}
{(c.sources ?? []).map((s) => ( - + {s} ))} diff --git a/apps/landing-page-astro/src/pages/unpack/compare/[pair].astro b/apps/landing-page-astro/src/pages/unpack/compare/[pair].astro index d5229ef9..28a68e52 100644 --- a/apps/landing-page-astro/src/pages/unpack/compare/[pair].astro +++ b/apps/landing-page-astro/src/pages/unpack/compare/[pair].astro @@ -10,11 +10,11 @@ export function getStaticPaths() { .filter((p) => p.a && p.b) .map(({ a, b, angle }) => ({ params: { pair: `${a.slug}-vs-${b.slug}` }, - props: { a, b, angle }, + props: { a, b, angle, pair: `${a.slug}-vs-${b.slug}` }, })); } -const { a, b, angle } = Astro.props; +const { a, b, angle, pair } = Astro.props; const rows = [ ['Files scanned', a.stats.filesScanned, b.stats.filesScanned], ['Size', a.stats.size, b.stats.size], @@ -45,6 +45,7 @@ const rows = [
← All unpacks diff --git a/apps/macos/Config/Shared.xcconfig b/apps/macos/Config/Shared.xcconfig index fd9e82cf..e22a8da1 100644 --- a/apps/macos/Config/Shared.xcconfig +++ b/apps/macos/Config/Shared.xcconfig @@ -15,8 +15,8 @@ EXECUTABLE_NAME = CodeVetterNative // Debug stays isolated from user data; Release owns the production identity. PRODUCT_BUNDLE_IDENTIFIER = com.codevetter.desktop PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = com.codevetter.desktop.native-preview -MARKETING_VERSION = 1.14.1 -CURRENT_PROJECT_VERSION = 11401 +MARKETING_VERSION = 1.14.2 +CURRENT_PROJECT_VERSION = 11402 // ========================================== // Platform Configuration diff --git a/benchmarks/repo-unpacks/prisma.json b/benchmarks/repo-unpacks/prisma.json index 25c53fb3..9fadc767 100644 --- a/benchmarks/repo-unpacks/prisma.json +++ b/benchmarks/repo-unpacks/prisma.json @@ -32410,64 +32410,6 @@ "agent_handoff": { "summary": "Agents should treat this as a layered contract compiler plus a runtime, not as a Next.js app. Start from Agents.md, the architecture overview, and architecture.config.json. Use pnpm on Node 24, rebuild before typecheck, and do not bypass lint:deps. Tests are written first and run with Vitest; name the Contract type at every JSON boundary; never use any, zod, or a bare cast. Skills and rules have one canonical directory each. The traps that waste a session are stale dist types, plane violations, fixture drift, warn-only marker checks, and editing a gitignored symlink tree.", "claims": [ - { - "claim": "Agents.md requires Node from the shell (engines.node in the root package.json; do not run nvm/fnm), pnpm instead of npm, and never npx. Build with pnpm build. Use the repo scripts pnpm typecheck and pnpm test rather than hand-rolled tsc or vitest. Use arktype, not zod. Never add extensions to TypeScript imports.", - "sources": [ - "Agents.md", - "package.json#L6-L62", - "package.json#L101-L103" - ], - "kind": "evidence" - }, - { - "claim": "Type rules from Agents.md: never any; never @ts-expect-error outside negative type tests; never @ts-nocheck; never suppress biome lints; no bare as in production code. Use blindCast or castAs from @internal/utils/casts. as const and test files are exempt. pnpm lint:casts is the ratchet.", - "sources": [ - "Agents.md", - "package.json#L36" - ], - "kind": "evidence" - }, - { - "claim": "Write tests before changing implementation. Omit the word should in test names. In sql-orm-client tests, assert the whole result shape with toEqual or a snapshot and an explicit select. Do not reexport from one file to another except under exports/ folders. Do not add backwards-compat exports unless asked. Do not branch on target; add an adapter.", - "sources": [ - "Agents.md" - ], - "kind": "evidence" - }, - { - "claim": "Where JSON enters the runtime, name the contract.d.ts type: postgres({ contractJson, url }) or validateSqlContractFully(contractJson). Do not use a wide generic such as SqlContract. The postgres() overloads encode that split between a live contract object and contractJson.", - "sources": [ - "Agents.md", - "packages/3-extensions/postgres/src/runtime/postgres.ts#L155-L170" - ], - "kind": "evidence" - }, - { - "claim": "After changing exported types in a package other packages consume, rebuild that package so dist/*.d.mts updates before downstream typecheck. Turbo already makes typecheck depend on build and ^build because self-imports of published subpaths read dist.", - "sources": [ - "Agents.md", - "turbo.json#L35-L43" - ], - "kind": "evidence" - }, - { - "claim": "Safe command set for a small change: pnpm build for the touched package, pnpm test:packages or that package's vitest, pnpm typecheck:packages, and pnpm lint:deps if imports moved. Contract artifact edits need pnpm fixtures:check rather than a hand-rolled emit-and-diff. Integration behavior needs pnpm test:integration; CLI journeys need pnpm test:journeys.", - "sources": [ - "package.json#L14-L23", - "package.json#L34", - "package.json#L57-L60", - "Agents.md" - ], - "kind": "evidence" - }, - { - "claim": "Edit skills at skills-contrib//SKILL.md and rules at .agents/rules/.mdc. .claude, .cursor, and .agents skill/rule trees are presentation symlinks materialized by prepare (skills add and scripts/sync-agent-rules.mjs). A rule added only under .cursor/rules is gitignored and lost. pnpm rules:sync and pnpm lint:rules:symlinks check the trees.", - "sources": [ - "Agents.md", - "package.json#L42-L69" - ], - "kind": "evidence" - }, { "claim": "New packages must be registered in architecture.config.json with plane-specific globs, broad to specific, including a migration glob for src/core/migrations/** even if the directory is empty. pnpm lint:deps fails the PR if a migration file imports a runtime package.", "sources": [ diff --git a/benchmarks/repo-unpacks/rustdesk.json b/benchmarks/repo-unpacks/rustdesk.json index fed8cc3a..e388d689 100644 --- a/benchmarks/repo-unpacks/rustdesk.json +++ b/benchmarks/repo-unpacks/rustdesk.json @@ -11139,14 +11139,6 @@ ], "kind": "evidence" }, - { - "claim": "libs/hbb_common is a git submodule (url https://github.com/rustdesk/hbb_common) that supplies rendezvous_proto, message framing, socket_client, Config/LocalConfig/PeerConfig, tokio re-exports and sodiumoxide crypto; in this clone the directory is empty, so no code under it can be read or built from this checkout.", - "sources": [ - ".gitmodules", - "libs/hbb_common" - ], - "kind": "evidence" - }, { "claim": "crates-io is patched: `libxdo-sys` is replaced by libs/libxdo-sys-stub (so builds work without libxdo on Wayland-only systems), and `tungstenite`, `webrtc`, `webrtc-util`, `webrtc-sctp` are pinned by git rev to rustdesk-org forks carrying custom fixes (incremental ws read buffer, Windows IPv6 ICE, SCTP congestion-control off by default).", "sources": [ @@ -11724,14 +11716,6 @@ ], "kind": "evidence" }, - { - "claim": "libs/hbb_common is empty in this clone — all wire protocol, Config, socket_client, and sodiumoxide usage is invisible to audit here; any build/test requires `git submodule update --init` first.", - "sources": [ - ".gitmodules", - "libs/hbb_common" - ], - "kind": "evidence" - }, { "claim": "Forked network stack risk: webrtc/webrtc-sctp/webrtc-util and tungstenite are pinned to rustdesk-org forks with custom congestion-control and parsing changes; upstream CVE fixes will not flow through cargo update and must be cherry-picked by rev.", "sources": [ @@ -11868,15 +11852,6 @@ ], "kind": "evidence" }, - { - "claim": "Bootstrap trap: `git submodule update --init libs/hbb_common` is mandatory — the directory is empty in this clone and every protocol symbol (RendezvousMessage, Config, Stream, socket_client, tokio re-export) resolves from it; build also needs flutter_rust_bridge codegen for `bridge_generated.rs` when the `flutter` feature is on.", - "sources": [ - ".gitmodules", - "libs/hbb_common", - "src/lib.rs#L34-39" - ], - "kind": "evidence" - }, { "claim": "Toolchain pins: rust-version 1.75 (Cargo.toml), CI RUST_VERSION 1.75 / macOS 1.81, Flutter 3.24.5 (3.44.9 win-arm64), flutter_rust_bridge '=1.80'/1.80.1 — mismatched frb versions regenerate incompatible bindings.", "sources": [ diff --git a/benchmarks/repo-unpacks/terminal.json b/benchmarks/repo-unpacks/terminal.json index 4fcc2fd1..10ed6cd9 100644 --- a/benchmarks/repo-unpacks/terminal.json +++ b/benchmarks/repo-unpacks/terminal.json @@ -11338,15 +11338,6 @@ ], "kind": "evidence" }, - { - "claim": "Renderers are pluggable behind renderer/base's IRenderer: AtlasEngine (D2D/D3D, custom shaders) for the new terminal, a GDI engine under renderer/gdi for legacy conhost, plus renderer/uia and renderer/wddmcon variants.", - "sources": [ - "src/renderer/atlas/AtlasEngine.cpp", - "src/renderer/base/Renderer.cpp", - "src/renderer/gdi" - ], - "kind": "evidence" - }, { "claim": "Settings layering is tag-based: ParsedSettings entries carry an OriginTag (InBox, User, Generated, Dynamic, Fragment) and SettingsLoader merges them in a fixed order — new settings sources plug in as additional origins without changing the merge engine.", "sources": [ diff --git a/biome.json b/biome.json index 8f135560..5fc010a9 100644 --- a/biome.json +++ b/biome.json @@ -32,7 +32,8 @@ "!**/*.svg", "!**/*.astro", "!benchmarks/public-catch-rate/cases", - "!benchmarks/repo-unpacks" + "!benchmarks/repo-unpacks", + "!apps/landing-page-astro/src/data/unpack-reports.json" ] }, "formatter": { diff --git a/crates/codevetter-core/Cargo.lock b/crates/codevetter-core/Cargo.lock index 436c3864..d741d6af 100644 --- a/crates/codevetter-core/Cargo.lock +++ b/crates/codevetter-core/Cargo.lock @@ -297,7 +297,7 @@ dependencies = [ [[package]] name = "codevetter-core" -version = "1.14.1" +version = "1.14.2" dependencies = [ "base64", "chromiumoxide", @@ -339,7 +339,7 @@ dependencies = [ [[package]] name = "codevetter-transport" -version = "1.14.1" +version = "1.14.2" dependencies = [ "codevetter-transport-macros", "tokio", @@ -347,7 +347,7 @@ dependencies = [ [[package]] name = "codevetter-transport-macros" -version = "1.14.1" +version = "1.14.2" [[package]] name = "colorchoice" diff --git a/crates/codevetter-core/Cargo.toml b/crates/codevetter-core/Cargo.toml index 5270aef6..2c821955 100644 --- a/crates/codevetter-core/Cargo.toml +++ b/crates/codevetter-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codevetter-core" -version = "1.14.1" +version = "1.14.2" edition = "2021" publish = false description = "CodeVetter verification core, CLI, and read-only MCP server" @@ -24,7 +24,7 @@ path = "src/bin/codevetter-graph.rs" [dependencies] # Source-compatible command facade while legacy annotations are renamed. This # is repository-owned and contains no Tauri, WebView, or windowing runtime. -tauri = { package = "codevetter-transport", version = "=1.14.1", path = "../codevetter-transport" } +tauri = { package = "codevetter-transport", version = "=1.14.2", path = "../codevetter-transport" } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" diff --git a/crates/codevetter-transport-macros/Cargo.toml b/crates/codevetter-transport-macros/Cargo.toml index 4f08cb5f..98c0a7bb 100644 --- a/crates/codevetter-transport-macros/Cargo.toml +++ b/crates/codevetter-transport-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codevetter-transport-macros" -version = "1.14.1" +version = "1.14.2" edition = "2021" publish = false diff --git a/crates/codevetter-transport/Cargo.toml b/crates/codevetter-transport/Cargo.toml index 68c38f25..90b40527 100644 --- a/crates/codevetter-transport/Cargo.toml +++ b/crates/codevetter-transport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codevetter-transport" -version = "1.14.1" +version = "1.14.2" edition = "2021" publish = false @@ -8,5 +8,5 @@ publish = false name = "tauri" [dependencies] -codevetter-transport-macros = { version = "=1.14.1", path = "../codevetter-transport-macros" } +codevetter-transport-macros = { version = "=1.14.2", path = "../codevetter-transport-macros" } tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/scripts/collect-unpack-repo.mjs b/scripts/collect-unpack-repo.mjs index 357fdb7d..cbc5e101 100644 --- a/scripts/collect-unpack-repo.mjs +++ b/scripts/collect-unpack-repo.mjs @@ -28,6 +28,10 @@ const slug = .split('/')[1] .toLowerCase() .replace(/[^a-z0-9]+/g, '-'); +if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) { + console.error(`invalid slug ${JSON.stringify(slug)} — must match /^[a-z0-9][a-z0-9-]*$/`); + process.exit(1); +} const work = mkdtempSync(join(tmpdir(), 'unpack-pilot-')); try { diff --git a/scripts/finalize-devin-report.mjs b/scripts/finalize-devin-report.mjs index 9539415f..835ca654 100644 --- a/scripts/finalize-devin-report.mjs +++ b/scripts/finalize-devin-report.mjs @@ -5,17 +5,26 @@ // node scripts/finalize-devin-report.mjs --clone --report import { execFileSync } from 'node:child_process'; -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; const ROOT = new URL('..', import.meta.url).pathname; const CORPUS = join(ROOT, 'benchmarks/repo-unpacks'); const [slug, ...rest] = process.argv.slice(2); -const get = (f) => rest[rest.indexOf(f) + 1]; +const get = (f) => { + const i = rest.indexOf(f); + return i < 0 ? undefined : rest[i + 1]; +}; const cloneDir = get('--clone'); const reportPath = get('--report'); const runtimeMs = Number(get('--runtime-ms') ?? 0); const model = get('--model') ?? 'swe-2'; +if (!slug || !cloneDir || !reportPath) { + console.error( + 'usage: node scripts/finalize-devin-report.mjs --clone --report ' + ); + process.exit(1); +} const SECTIONS = [ 'system_map', @@ -49,25 +58,52 @@ if (missing.length) { process.exit(1); } +// Citations must resolve against the pinned commit object — `git cat-file -e +// :` rejects `../` traversal, files that only exist at a different +// revision, and untracked working-tree files alike. +const safePath = (p) => p && !p.startsWith('/') && !p.split('/').includes('..'); +const pinnedExists = (p) => { + if (!safePath(p)) return false; + try { + execFileSync('git', ['-C', cloneDir, 'cat-file', '-e', `${headSha}:${p}`], { + stdio: 'ignore', + }); + return true; + } catch { + return false; + } +}; + let total = 0; let bad = 0; +let dropped = 0; for (const key of SECTIONS) { for (const claim of report[key].claims) { - claim.sources = (claim.sources ?? []).map( - (s) => - s - .split('#')[0] - .trim() - .replace(/\s*\([^)]*\)\s*$/, '') + (s.includes('#L') ? `#L${s.split('#L')[1]}` : '') - ); + claim.sources = (claim.sources ?? []).map((s) => { + const path = s + .split('#')[0] + .trim() + .replace(/\s*\([^)]*\)\s*$/, ''); + return s.includes('#L') ? `${path}#L${s.split('#L')[1]}` : path; + }); for (const src of claim.sources) { total += 1; - if (!existsSync(join(cloneDir, src.split('#')[0]))) bad += 1; + if (!pinnedExists(src.split('#')[0])) bad += 1; } } + const kept = report[key].claims.filter((c) => + (c.sources ?? []).every((s) => pinnedExists(s.split('#')[0])) + ); + dropped += report[key].claims.length - kept.length; + report[key].claims = kept; +} +console.log( + `${record.repo}: ${total} citations, ${bad} unresolvable${dropped ? `, ${dropped} claims dropped` : ''}` +); +if (total === 0 || SECTIONS.some((k) => report[k].claims.length === 0)) { + console.error(`${slug}: report unusable after pruning — not merging`); + process.exit(1); } -console.log(`${record.repo}: ${total} citations, ${bad} unresolvable`); -if (total === 0) process.exit(1); record.report = report; record.analysis = { diff --git a/scripts/sync-unpack-pages.mjs b/scripts/sync-unpack-pages.mjs index f3b51645..64faacd6 100644 --- a/scripts/sync-unpack-pages.mjs +++ b/scripts/sync-unpack-pages.mjs @@ -189,12 +189,18 @@ for (const file of readdirSync(CORPUS) ? { summary: record.report[k], claims: [] } : { summary: record.report[k].summary, - claims: (record.report[k].claims ?? []).map((c) => ({ - ...c, - // Agents occasionally append "(symbol)" after a path — strip it - // so the source links resolve. - sources: (c.sources ?? []).map((s) => s.replace(/\s*\([^)]*\)\s*$/, '')), - })), + // Keep only well-formed claims — a malformed record must not + // reach the published JSON. + claims: (record.report[k].claims ?? []) + .filter((c) => typeof c?.claim === 'string' && Array.isArray(c.sources)) + .map((c) => ({ + ...c, + // Agents occasionally append "(symbol)" after a path — strip it + // so the source links resolve. + sources: c.sources + .filter((s) => typeof s === 'string' && s.length) + .map((s) => s.replace(/\s*\([^)]*\)\s*$/, '')), + })), }, ]) ) diff --git a/scripts/verify-unpack-claims.mjs b/scripts/verify-unpack-claims.mjs index d933c598..6ea0a9b9 100644 --- a/scripts/verify-unpack-claims.mjs +++ b/scripts/verify-unpack-claims.mjs @@ -1,11 +1,15 @@ #!/usr/bin/env node // Verifies every cited source path in analyzed corpus records against the -// pinned clone on disk. With --prune, drops claims whose sources don't all -// resolve (they'd 404 as GitHub links) and reports what was removed. +// pinned commit object in the local clone — `git cat-file -e :` +// proves the file existed at the scanned commit and rejects `../` traversal, +// later-revision-only files, and untracked working-tree files. With --prune, +// drops claims whose sources don't all resolve and exits nonzero if any +// section was emptied. // // node scripts/verify-unpack-claims.mjs --clones /tmp/unpack-pilot [--prune] -import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; const ROOT = new URL('..', import.meta.url).pathname; @@ -21,10 +25,36 @@ const normalize = (s) => .trim() .replace(/\s*\([^)]*\)\s*$/, ''); +const safePath = (p) => p && !p.startsWith('/') && !p.split('/').includes('..'); +const pinnedExists = (clone, sha, p) => { + if (!safePath(p)) return false; + try { + execFileSync('git', ['-C', clone, 'cat-file', '-e', `${sha}:${p}`], { stdio: 'ignore' }); + return true; + } catch { + return false; + } +}; + +let failures = 0; for (const file of readdirSync(CORPUS).filter((f) => f.endsWith('.json'))) { const record = JSON.parse(readFileSync(join(CORPUS, file), 'utf8')); if (!record.report) continue; const clone = join(clonesDir, CLONE_NAMES[record.slug] ?? record.slug); + const sha = record.scan?.inventory?.commit_sha; + if (!sha) { + console.log(`${record.slug}: no pinned commit — skipped`); + continue; + } + const head = execFileSync('git', ['-C', clone, 'rev-parse', 'HEAD'], { + encoding: 'utf8', + }).trim(); + if (head !== sha) { + console.log( + `${record.slug}: clone at ${head.slice(0, 7)}, scan pinned to ${sha.slice(0, 7)} — skipped` + ); + continue; + } let total = 0; let bad = 0; let dropped = 0; @@ -33,22 +63,27 @@ for (const file of readdirSync(CORPUS).filter((f) => f.endsWith('.json'))) { for (const claim of section.claims) { for (const source of claim.sources ?? []) { total += 1; - if (!existsSync(join(clone, normalize(source)))) bad += 1; + if (!pinnedExists(clone, sha, normalize(source))) bad += 1; } } if (prune) { const kept = section.claims.filter((c) => - (c.sources ?? []).every((s) => existsSync(join(clone, normalize(s)))) + (c.sources ?? []).every((s) => pinnedExists(clone, sha, normalize(s))) ); dropped += section.claims.length - kept.length; section.claims = kept; } } - if (bad || prune) { + if (bad || dropped) { console.log( `${record.slug}: ${total} cited, ${bad} unresolvable${dropped ? `, ${dropped} claims dropped` : ''}` ); + if (bad) failures += 1; if (prune && dropped) writeFileSync(join(CORPUS, file), `${JSON.stringify(record, null, 2)}\n`, 'utf8'); } } +if (failures) { + console.error(`${failures} record(s) with unresolvable citations`); + process.exit(1); +}