--- whymark: 1 title: Make AI code reviews compact and evidence-aware author: GPT-6 (Codex) date: 2026-09-17T09:11:41.568Z scope: worktree base: HEAD@28e5280 head: working-tree repo: spinkdev/whymark summary: Reviewers can now select overlapping annotations, collapse comments, keep semantic diff colors on hover, and synchronize split scrolling through settings. The npm package installs the agent skill without Git access. Verification records and deterministic quality reports distinguish author claims, current source, stale evidence and unavailable checks. The first scanner delivery supports ESLint and configured commands; optional adapters remain deferred. checks: - cmd: npm test status: pass detail: exit 0 in 5.1s ran: 2026-09-17T09:13:17.294Z - cmd: npm run typecheck status: pass detail: exit 0 in 1.5s ran: 2026-09-17T09:13:18.794Z - cmd: npm run lint status: pass detail: exit 0 in 2.8s ran: 2026-09-17T09:13:21.633Z - cmd: node tests/e2e/improvements.mjs status: pass detail: exit 0 in 6.7s ran: 2026-09-17T09:13:28.322Z - cmd: npm run test:ui status: pass detail: exit 0 in 45.1s ran: 2026-09-17T09:14:13.430Z - cmd: npm run build -- --webpack status: pass detail: exit 0 in 10.8s ran: 2026-09-17T09:14:24.185Z evidence: - version: 1 command: npm test noteId: null file: null claimed: unknown status: pass ran: 2026-09-17T09:14:24.350Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm run typecheck noteId: null file: null claimed: unknown status: pass ran: 2026-09-17T09:14:24.352Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run lint noteId: null file: null claimed: unknown status: pass ran: 2026-09-17T09:14:24.354Z durationMs: 2839 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: bd33d3e9d252051385f8223926dc7c33d4b7c5c8341c03a626d16d8366878d3b outputArtifact: .artifacts/whymark/bd33d3e9d252051385f8223926dc7c33d4b7c5c8341c03a626d16d8366878d3b.log summary: exit 0 in 2.8s - version: 1 command: node tests/e2e/improvements.mjs noteId: null file: null claimed: unknown status: pass ran: 2026-09-17T09:14:24.357Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: npm run test:ui noteId: null file: null claimed: unknown status: pass ran: 2026-09-17T09:14:24.359Z durationMs: 45108 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 933c13f502a0eb57c1c4684e4915bc184504bc261fad969ec7f9d8297ce1d789 outputArtifact: .artifacts/whymark/933c13f502a0eb57c1c4684e4915bc184504bc261fad969ec7f9d8297ce1d789.log summary: exit 0 in 45.1s - version: 1 command: npm run build -- --webpack noteId: null file: null claimed: unknown status: pass ran: 2026-09-17T09:14:24.361Z durationMs: 10755 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 00a63c0d4e1d02285597aea6921a8d9acacc174c97affa60e49fa3dc6e07e071 outputArtifact: .artifacts/whymark/00a63c0d4e1d02285597aea6921a8d9acacc174c97affa60e49fa3dc6e07e071.log summary: exit 0 in 10.8s - version: 1 command: npm test noteId: improvement-1 file: .agents/skills/whymark/SKILL.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.363Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-2 file: .agents/skills/whymark/SKILL.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.365Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-3 file: .agents/skills/whymark/SKILL.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.367Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm run build:cli noteId: improvement-7 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.369Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-8 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.371Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-9 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.373Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-10 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.375Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-11 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.377Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-12 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.379Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-13 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.381Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-14 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.383Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-15 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.385Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-16 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.387Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-17 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.389Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-18 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.391Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-19 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.393Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-20 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.395Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-21 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.397Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-22 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.399Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-23 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.401Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-24 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.403Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-25 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.405Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-26 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.407Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-27 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.409Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-28 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.411Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-29 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.413Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-30 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.415Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-31 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.416Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-32 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.418Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-33 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.420Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-34 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.422Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-35 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.424Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-36 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.426Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-37 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.428Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-38 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.430Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run build:cli noteId: improvement-39 file: dist/whymark.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.432Z durationMs: 134 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e outputArtifact: .artifacts/whymark/78a32c4b00be8f150c251616422845ecfca20c0f0761a85556b8bde3bfcb420e.log summary: exit 0 in 0.1s - version: 1 command: npm run lint noteId: improvement-40 file: eslint.config.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.434Z durationMs: 2839 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: bd33d3e9d252051385f8223926dc7c33d4b7c5c8341c03a626d16d8366878d3b outputArtifact: .artifacts/whymark/bd33d3e9d252051385f8223926dc7c33d4b7c5c8341c03a626d16d8366878d3b.log summary: exit 0 in 2.8s - version: 1 command: npm test noteId: improvement-41 file: package.json claimed: unknown status: pass ran: 2026-09-17T09:14:24.436Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-42 file: package.json claimed: unknown status: pass ran: 2026-09-17T09:14:24.438Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-43 file: package.json claimed: unknown status: pass ran: 2026-09-17T09:14:24.440Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-44 file: package.json claimed: unknown status: pass ran: 2026-09-17T09:14:24.442Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-45 file: prompts/whymark-author.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.444Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-46 file: spec/whymark-v1.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.446Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-47 file: spec/whymark-v1.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.447Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-48 file: spec/whymark-v1.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.449Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-49 file: spec/whymark-v1.md claimed: unknown status: pass ran: 2026-09-17T09:14:24.451Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm run typecheck noteId: improvement-50 file: src/app/r/[slug]/page.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.453Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-51 file: src/app/r/[slug]/page.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.455Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm test noteId: improvement-52 file: src/cli/help.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.457Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-53 file: src/cli/help.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.459Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-54 file: src/cli/whymark.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.461Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-55 file: src/cli/whymark.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.463Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-56 file: src/cli/whymark.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.465Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-57 file: src/cli/whymark.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.467Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-58 file: src/cli/whymark.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.469Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-59 file: src/cli/whymark.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.471Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-60 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.473Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-61 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.475Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-62 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.477Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-63 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.479Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-64 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.481Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-65 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.482Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-66 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.484Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-67 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.486Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-68 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.488Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-69 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.490Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-70 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.492Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-71 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.494Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-72 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.496Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-73 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.498Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-74 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.500Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-75 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.502Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-76 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.504Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-77 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.506Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-78 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.508Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-79 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.510Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-80 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.512Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-81 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.514Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-82 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.516Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-83 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.518Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-84 file: src/components/whymark/file-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.520Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-85 file: src/components/whymark/note-card.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.522Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-86 file: src/components/whymark/note-card.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.524Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-87 file: src/components/whymark/note-card.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.526Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-88 file: src/components/whymark/note-card.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.528Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-89 file: src/components/whymark/note-card.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.530Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: npm run typecheck noteId: improvement-90 file: src/components/whymark/pills.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.532Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-91 file: src/components/whymark/pills.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.534Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-92 file: src/components/whymark/pills.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.536Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-93 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.538Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-94 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.540Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-95 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.541Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-96 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.543Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-97 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.545Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-98 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.547Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-99 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.549Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-100 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.551Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-101 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.553Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-102 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.555Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-103 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.557Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-104 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.559Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-105 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.561Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-106 file: src/components/whymark/review-view.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.563Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-107 file: src/lib/view-model.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.565Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-108 file: src/lib/view-model.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.567Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-109 file: src/lib/view-model.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.569Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm run typecheck noteId: improvement-110 file: src/lib/view-model.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.571Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: npm test noteId: improvement-111 file: src/lib/whymark/parse.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.573Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-112 file: src/lib/whymark/parse.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.575Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-113 file: src/lib/whymark/parse.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.577Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-114 file: src/lib/whymark/parse.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.579Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-115 file: src/lib/whymark/parse.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.580Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-116 file: src/lib/whymark/parse.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.582Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-117 file: src/lib/whymark/serialize.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.584Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-118 file: src/lib/whymark/stats.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.586Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-119 file: src/lib/whymark/types.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.588Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-120 file: src/lib/whymark/types.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.590Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-121 file: src/lib/whymark/validate-tree.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.592Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-122 file: src/lib/whymark/validate-tree.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.594Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-123 file: src/lib/whymark/validate.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.596Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-124 file: src/lib/whymark/validate.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.598Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-125 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.600Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-126 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.602Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-127 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.604Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-128 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.606Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-129 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.608Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-130 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.610Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-131 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.612Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-132 file: src/lib/whymark/verify.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.614Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm run test:ui noteId: improvement-133 file: tests/e2e/viewer.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.616Z durationMs: 45108 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 933c13f502a0eb57c1c4684e4915bc184504bc261fad969ec7f9d8297ce1d789 outputArtifact: .artifacts/whymark/933c13f502a0eb57c1c4684e4915bc184504bc261fad969ec7f9d8297ce1d789.log summary: exit 0 in 45.1s - version: 1 command: npm test noteId: improvement-136 file: src/cli/quality.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.618Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm run typecheck noteId: improvement-137 file: src/components/whymark/evidence-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.620Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-138 file: src/components/whymark/note-disclosure.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.622Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: npm run typecheck noteId: improvement-139 file: src/components/whymark/quality-panel.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.624Z durationMs: 1499 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee outputArtifact: .artifacts/whymark/c8e1ffdd0432338833d871a6e343b808199440909e94deafa18f740403ea98ee.log summary: exit 0 in 1.5s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-140 file: src/components/whymark/review-settings.tsx claimed: unknown status: pass ran: 2026-09-17T09:14:24.626Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-141 file: src/components/whymark/use-review-preferences.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.628Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: npm test noteId: improvement-142 file: src/lib/quality/baseline.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.630Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-143 file: src/lib/quality/import.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.632Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-144 file: src/lib/quality/run.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.634Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-145 file: src/lib/quality/types.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.636Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-146 file: src/lib/review-preferences.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.638Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: npm test noteId: improvement-147 file: src/lib/skill/install.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.639Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-148 file: src/lib/whymark/evidence-node.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.641Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-149 file: src/lib/whymark/evidence.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.643Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: node tests/e2e/improvements.mjs noteId: improvement-150 file: tests/e2e/improvements.mjs claimed: unknown status: pass ran: 2026-09-17T09:14:24.645Z durationMs: 6689 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: 7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f outputArtifact: .artifacts/whymark/7ff5052f25b690b22504e96ee09352fc10c2627cf6c0ec5362cb903b58766d3f.log summary: exit 0 in 6.7s - version: 1 command: npm test noteId: improvement-151 file: tests/evidence.test.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.647Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-152 file: tests/fixtures/multiple-notes.whymark claimed: unknown status: pass ran: 2026-09-17T09:14:24.649Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-153 file: tests/improvements.test.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.651Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-154 file: tests/package.test.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.653Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-155 file: tests/quality-cli.test.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.655Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-156 file: tests/quality.test.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.657Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-157 file: tests/skill-install.test.ts claimed: unknown status: pass ran: 2026-09-17T09:14:24.659Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s - version: 1 command: npm test noteId: improvement-158 file: whymark.config.json claimed: unknown status: pass ran: 2026-09-17T09:14:24.661Z durationMs: 5085 exitCode: 0 cwd: . tool: whymark@0.2.0 runtime: v22.23.2 source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 base: HEAD@28e5280 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a outputHash: af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae outputArtifact: .artifacts/whymark/af05fae140ddc6908d27c8e10c03914b953c7ecf15c050ff6a3c9cb4b0a01cae.log summary: exit 0 in 5.1s quality: version: 1 ran: 2026-09-17T09:14:30.671Z source: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 sourceAfter: 76bff8782ca960f91def68033d6f79b15d35d992d6fb1733b219f14f1053f142 head: 28e528059f4253d00ee07f3f0a65dad7f44717e0 clean: false base: 28e528059f4253d00ee07f3f0a65dad7f44717e0 reviewHash: 6d17b1d57a15d8db7181a79db24a9c4036a7a761c37c0ceb3f1313981010210a provenance: local-run comparison: unavailable checks: - id: eslint version: v9.39.5 command: "node node_modules/eslint/bin/eslint.js . --format json --rule 'complexity: [warn, 15]' --rule 'max-depth: [warn, 4]'" status: pass exitCode: 0 outputHash: b533372416ab4519fb5859a36dccd21c21933aa7da0362ccffb1bd7439e12ddc outputArtifact: .artifacts/whymark/b533372416ab4519fb5859a36dccd21c21933aa7da0362ccffb1bd7439e12ddc.log detail: "" - id: typecheck version: Version 5.9.3 command: npm run typecheck status: pass exitCode: 0 outputHash: 0f80c6df94eee3539cd51fc9109c419630ed9af937aaac2b7f410006fc3cc98b outputArtifact: .artifacts/whymark/0f80c6df94eee3539cd51fc9109c419630ed9af937aaac2b7f410006fc3cc98b.log detail: "" findings: - id: d3703320a922c762ec4e6a882a2586c60d3975a9005663537cdd99b74a0be819 tool: eslint rule: complexity severity: warning message: Function 'main' has a complexity of 22. Maximum allowed is 15. path: src/cli/whymark.ts line: 115 endLine: 115 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 500a68c31720a67642aa252b06a986b0cbf474a6cc258ff735b53b28e5caa57f tool: eslint rule: complexity severity: warning message: Function 'FilePanel' has a complexity of 30. Maximum allowed is 15. path: src/components/whymark/file-panel.tsx line: 103 endLine: 103 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 5c59b2818dc47c2fff02328d096c668a088fdb40aec838dd40aaaf3070c4d11f tool: eslint rule: complexity severity: warning message: Function 'UnifiedRow' has a complexity of 23. Maximum allowed is 15. path: src/components/whymark/file-panel.tsx line: 639 endLine: 639 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 1fd1c21b1667729ea79be2e6339378fd0e4492e348b9e8d76282ebd77fd1d784 tool: eslint rule: complexity severity: warning message: Function 'SplitRow' has a complexity of 33. Maximum allowed is 15. path: src/components/whymark/file-panel.tsx line: 747 endLine: 747 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: cf9873b0a8061858845fd7265f5804b9cdb55f3c341c0f0e62928351aff06fd5 tool: eslint rule: complexity severity: warning message: Function 'toSplitRows' has a complexity of 22. Maximum allowed is 15. path: src/components/whymark/file-panel.tsx line: 1058 endLine: 1058 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: e479a479d73d14ce4e04657fb464d8a54ba6a501761dca1e28017eb6509c598e tool: eslint rule: complexity severity: warning message: Function 'renderBlocks' has a complexity of 28. Maximum allowed is 15. path: src/components/whymark/markdown.tsx line: 12 endLine: 12 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 5c14f8cd7020b7d4e27a1a8ab8749ca204ecfcf7a2e3d38425bd141451ae49b1 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/components/whymark/markdown.tsx line: 139 endLine: 143 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 566e26d3d565555452b14a1b7de928741ebe87418e63fcd8f279ec9761fb68ce tool: eslint rule: complexity severity: warning message: Function 'NoteCard' has a complexity of 21. Maximum allowed is 15. path: src/components/whymark/note-card.tsx line: 22 endLine: 22 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 4981ae73eb1ab24a517f814b6421f1ac018aca5d007dd929aa3a635da974f69b tool: eslint rule: complexity severity: warning message: Function 'ReviewView' has a complexity of 22. Maximum allowed is 15. path: src/components/whymark/review-view.tsx line: 47 endLine: 47 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: b8689bb51e90ab2933492b30dc7abd6c8d29414b9b1995218f53022695f70acd tool: eslint rule: complexity severity: warning message: Function 'readQuality' has a complexity of 22. Maximum allowed is 15. path: src/lib/quality/import.ts line: 13 endLine: 13 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 5a7d68c023ca99c650e08dda4eb8ff2223b4358fc25fb8001afb8e9ba210a4b6 tool: eslint rule: complexity severity: warning message: Arrow function has a complexity of 27. Maximum allowed is 15. path: src/lib/quality/import.ts line: 27 endLine: 27 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 8724418887276e467e22461d3e42eea60b353bdbbcaf5c08d193dad404a4aef8 tool: eslint rule: complexity severity: warning message: Function 'parseESLint' has a complexity of 29. Maximum allowed is 15. path: src/lib/quality/import.ts line: 43 endLine: 43 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 5a4e2bd03c709041c5e8d56257a5d015f1c7b32f31cadc0a88d1baf4109e604d tool: eslint rule: complexity severity: warning message: Async function 'scanQuality' has a complexity of 26. Maximum allowed is 15. path: src/lib/quality/run.ts line: 55 endLine: 55 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 0d22fa81ecd76d37299200772e33800b008e4e7f7846be847a6e8270b9f9a8d7 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/quality/run.ts line: 72 endLine: 72 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 6ab8dda08d4a942af8c311fdfa5c68c73b4e1fe47004a46224ea5fc233406e03 tool: eslint rule: complexity severity: warning message: Function 'installSkill' has a complexity of 19. Maximum allowed is 15. path: src/lib/skill/install.ts line: 15 endLine: 15 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 7b47bb8cf910ddaa79385d46e85c027b862aa348c436491e5f66f1a3fcd77d2b tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/evidence-node.ts line: 52 endLine: 52 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: b343881757974e1cf9ab92e7f0c72e65c80d08a8b7db1275b62892d51961876c tool: eslint rule: complexity severity: warning message: Arrow function has a complexity of 30. Maximum allowed is 15. path: src/lib/whymark/evidence.ts line: 39 endLine: 39 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 957d3de730731dd56097ffad4a36a4af54b6282bf48dc049726e3fec65e53661 tool: eslint rule: complexity severity: warning message: Function 'parseUnifiedDiff' has a complexity of 42. Maximum allowed is 15. path: src/lib/whymark/git.ts line: 204 endLine: 204 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: c6436710e4cf247d544db4fe3aefc5e415eb84521f780668be4ddc7a7a5859ff tool: eslint rule: complexity severity: warning message: Function 'parseWhymark' has a complexity of 29. Maximum allowed is 15. path: src/lib/whymark/parse.ts line: 96 endLine: 96 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 949843b98eb916df4ac3ae9c76a51be130c27e263673bf7e579e367109e435bd tool: eslint rule: complexity severity: warning message: Function 'parseFrontmatter' has a complexity of 37. Maximum allowed is 15. path: src/lib/whymark/parse.ts line: 263 endLine: 263 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 5e864a852b38e83af36b6c7bbce5b221842a6b9320be7ca625dad49e97c32c20 tool: eslint rule: complexity severity: warning message: Function 'parseNoteHeader' has a complexity of 31. Maximum allowed is 15. path: src/lib/whymark/parse.ts line: 686 endLine: 686 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: ed1055c99afdb86890feb4858632e9faeabfcc18fafbbe0d13a36163788d17c0 tool: eslint rule: complexity severity: warning message: Function 'setField' has a complexity of 31. Maximum allowed is 15. path: src/lib/whymark/parse.ts line: 864 endLine: 864 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 05fc65f410343170b6aef9cd4a270b7e730173d6cdd1bab1914ed7ed85ff96a6 tool: eslint rule: complexity severity: warning message: Function 'parseVerifyClaim' has a complexity of 17. Maximum allowed is 15. path: src/lib/whymark/parse.ts line: 1068 endLine: 1068 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 3823fcf85becefb3b07fd7e99a0a9fa5f4ee7c729ed7b1086e453b01202bdcd1 tool: eslint rule: complexity severity: warning message: Function 'serializeNote' has a complexity of 19. Maximum allowed is 15. path: src/lib/whymark/serialize.ts line: 125 endLine: 125 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 652b35ec270f6c28b15d3456ae1a6212a38380c5394b6c70edefc7e2d9a2502d tool: eslint rule: complexity severity: warning message: Function 'collectSourceDiagnostics' has a complexity of 31. Maximum allowed is 15. path: src/lib/whymark/validate-tree.ts line: 60 endLine: 60 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 8b4ae5532f0379ee707caa7ac441a6d0583762627971871c27169b08fbd33e9c tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 67 endLine: 67 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 8a779d0c86505d24afa72756b496826eff32a5ac00142bbd11ae18b27f6d06a9 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 69 endLine: 69 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: f294afe504991c8d2842967d28fb6dc929a7a6d9ff6b66e942cc477125d97af6 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 72 endLine: 72 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 58fbaa27da57ab019a47232710c8d33dffb95cc8bda0d852bc833e319d2d7eb4 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 73 endLine: 80 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: b4c02b1ad0fe1b29370767bfb27f6a64231e3ceda08a73235938d016c088c8c3 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (6). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 78 endLine: 78 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 4a09a4cb9b41df70c6e61632aa9ba8b31ce0d17e952ccc76cfca6634f0e38be2 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 82 endLine: 82 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 656e61c8514964c408ce785828d16fe169b7b778a42b67b719ed4d73f4e549c5 tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 84 endLine: 84 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: d0835ea1a0885b5d4874b8ab39bcae90ed37e9e32caf7928f84b3fe661ad3c8c tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 88 endLine: 88 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: 492df55a5607b41f41f233aae854294e43171709ac99a01689c7efd298d1c5cf tool: eslint rule: max-depth severity: warning message: Blocks are nested too deeply (5). Maximum allowed is 4. path: src/lib/whymark/validate-tree.ts line: 90 endLine: 90 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/max-depth - id: b76c25119ef842e669d98b6524af769533360d6b267565e43fad43367e72fdb7 tool: eslint rule: complexity severity: warning message: Function 'validateDocument' has a complexity of 44. Maximum allowed is 15. path: src/lib/whymark/validate.ts line: 31 endLine: 31 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity - id: 6a0d775a9de8c335abddd05d1cd11d4a2be5d4dec54bc796636243c7f92b9bf8 tool: eslint rule: complexity severity: warning message: Function 'verifyDocument' has a complexity of 21. Maximum allowed is 15. path: src/lib/whymark/verify.ts line: 63 endLine: 63 status: uncompared helpUrl: https://eslint.org/docs/latest/rules/complexity --- @file .agents/skills/whymark/SKILL.md modified +21 -2 oldsha=0cec792 newsha=03ff047 @@ -51,7 +51,8 @@ npx whymark stats reviews/.whymark real result. Run it. If a claim is contradicted, fix the code or the claim before handing the review over. -Aim for **≥80% line coverage** and zero stubs. Then tell the user to open the +Aim for zero stubs and meaningful coverage of decisions. Do not add filler merely +to reach a percentage. Then tell the user to open the review at `/r/` (`npm run dev`). ## Annotating @@ -77,7 +78,11 @@ means two annotations. `generated`, `note`. **`risk`** = blast radius if you are wrong (`low`/`medium`/`high`). -**`confidence`** = 0..1, honest. Low confidence with a `question:` is far more +**`urgency`** = action priority (`info`/`normal`/`urgent`/`blocking`), independent +of risk. Emit it as a body field, e.g. `urgency: urgent`, before `why:` so older +v1 readers preserve it. Several notes can cover the same line for different reasons. + +**`confidence`** = 0..1, an author estimate, not measured accuracy. Low confidence with a `question:` is far more useful to a reviewer than false certainty. ### `why` @@ -160,6 +165,20 @@ line someone will reject, and a note spanning twenty lines gives them nothing to act on. When one line carries two changes and only one of them is defensible, say which part is which: that is a decision the reviewer can make in one click. +## Execution and quality evidence + +`verify --write` saves command results and source fingerprints in `evidence` +frontmatter, with output logs under `.artifacts/whymark/`. Record conflicts and +unavailable checks honestly. A matching hash establishes identity, not authenticity; +passing tests and lint do not prove correctness. Inspect embedded commands before +explicitly rerunning a review; viewing/importing must never execute them. + +Optional deterministic checks: `npx whymark quality init` creates local configuration. +Read its commands before `npx whymark quality reviews/.whymark --run --write`. +This first adapter supports ESLint JSON and existing check commands, with no AI. +Missing tools and failed baselines remain unavailable. Do not change repository +quality policy or suppress findings without a concrete reason and expiry. + ## Anti-patterns - Narrating the code (`what:` on every line) instead of explaining it. @note +54..55 kind=intent id=improvement-1 urgency: normal why: Agents need to author precise, source-backed notes without manufacturing confidence or coverage. The installed guidance must explain the new metadata and explicit execution boundary using resources shipped in the package. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +81..85 kind=intent id=improvement-2 urgency: normal why: Agents need to author precise, source-backed notes without manufacturing confidence or coverage. The installed guidance must explain the new metadata and explicit execution boundary using resources shipped in the package. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +168..181 kind=intent id=improvement-3 urgency: normal why: Agents need to author precise, source-backed notes without manufacturing confidence or coverage. The installed guidance must explain the new metadata and explicit execution boundary using resources shipped in the package. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file AGENTS.md modified +4 oldsha=7113c49 newsha=a87407c @@ -10,5 +10,9 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex # whymark +Read `~/.vault/projects/whymark/README.md` for shared project context before work. +Feature requirements and delivery plans live in `specs/`; the review file format +remains in `spec/whymark-v1.md`. + This repository defines the whymark review format (`spec/whymark-v1.md`), its tooling (`src/lib/whymark`, `src/cli/whymark.ts`), and a viewer for it (`src/app`). @note +13..16 kind=intent id=improvement-4 urgency: normal why: Keep the implementation and its acceptance criteria together so a later agent can continue the approved scope without reconstructing decisions. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: review "Compared documentation with implemented commands and the recorded scope" => pass @file CLAUDE.md modified +2 oldsha=43c994c newsha=4cce2a2 @@ -1,1 +1,3 @@ @AGENTS.md + +@~/.vault/projects/whymark/README.md @note +2..3 kind=intent id=improvement-5 urgency: normal why: Keep the implementation and its acceptance criteria together so a later agent can continue the approved scope without reconstructing decisions. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: review "Compared documentation with implemented commands and the recorded scope" => pass @file README.md modified +61 oldsha=a88b102 newsha=c1448fb @@ -290,3 +290,64 @@ and takes none of its permissions away. Stating this in the licence is the same argument the format makes. A reader deciding whether to trust code is better served by knowing how it was produced and where the review stopped than by an unqualified assurance. + +## Agent skill from npm + +The private Git repository is not required. The npm package includes the skill, +format reference and a working example embedded in the skill. + +```sh +npx whymark@ skill install --dry-run +npx whymark@ skill install --agent codex --agent claude-code +# Optional: install in your user directory rather than this project +npx whymark@ skill install --agent codex --global +``` + +Replace `` with the published release. These commands are implemented in +this checkout; a new release must be published before they are available from npm. +The default target is project-local Codex. Identical files are unchanged; customized +files require `--force`. Symlink destinations are refused. Installation never +changes unrelated agent instruction files. `init` still means `new`, not skill installation. + +## Compact reviews and settings + +A line can carry multiple comments; its count button opens a selector for each. +Comments collapse independently while type, urgency, line selector and author +confidence stay visible. Missing confidence is shown as unknown. Settings control +horizontal split-pane synchronization, default disclosure and compact layout. +Preferences stay in this browser with a session fallback when storage is blocked. + +## Checks without AI + +```sh +npx whymark quality init +# Inspect whymark.config.json first; --run executes its local shell commands. +npx whymark quality reviews/change.whymark --run --write +npx whymark quality reviews/change.whymark --run > reviews/baseline.quality.json +npx whymark quality reviews/change.whymark --run --baseline reviews/baseline.quality.json --write +# Debounced rescans; superseded commands are cancelled. Ctrl-C stops watching. +npx whymark quality reviews/change.whymark --run --watch --write +# Import existing output without executing anything +npx whymark quality reviews/change.whymark --import eslint.json --tool-version 9 --write +``` + +Start with the generated ESLint and TypeScript commands and adapt them to your +repository. The ESLint command includes configurable complexity (15) and nesting (4) +warnings. Run checks in full project context; the viewer can filter findings to +changed lines. Missing tools are unavailable, not passing. A baseline needs a clean source checkout with HEAD matching the review's base +(short review hashes are resolved through Git) and matching tool versions/commands; +create it on the base checkout with configuration already committed. Comparison separates new, existing and resolved findings, handles +Git-detected renames, and ignores line shifts. Use a `.quality.json` file under +`reviews/` or outside the checkout for snapshots, so the report does not fingerprint +itself. Findings have stable IDs; `suppressions` entries in configuration require +`id`, `reason` and a future `expires` date. Checks are advisory to repository policy; +the command exits nonzero for failed/unavailable checks and unsuppressed errors. + +`verify --write` records execution metadata, output hashes and source fingerprints. +The local viewer checks source freshness and execution log hashes; imported evidence +remains unauthenticated. “Claimed verified” is an author claim, not measured code +accuracy. Fingerprints exclude ignored files, `.artifacts/`, and review outputs in +`reviews/`; they include tests/config/lockfiles but do not cover installed dependency +bytes or external services. Inspect output logs before sharing. No command runs just +by opening a review. Optional Knip, Semgrep, audit and coverage adapters remain future +integrations; the current quality adapter is ESLint plus configured pass/fail checks. @note +293..353 kind=intent id=improvement-6 urgency: normal why: Keep the implementation and its acceptance criteria together so a later agent can continue the approved scope without reconstructing decisions. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: review "Compared documentation with implemented commands and the recorded scope" => pass @file dist/whymark.mjs modified +647 -48 oldsha=19eaf3a newsha=b855efb @@ -2,8 +2,8 @@ /* generated by npm run build:cli — do not edit */ // src/cli/whymark.ts -import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join as join2, relative, resolve } from "node:path"; +import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs"; +import { dirname as dirname2, join as join5, relative as relative6, resolve as resolve6 } from "node:path"; import { fileURLToPath } from "node:url"; // src/lib/whymark/parse.ts @@ -26,6 +26,7 @@ var NOTE_KINDS = [ "generated", "note" ]; +var URGENCIES = ["info", "normal", "urgent", "blocking"]; var VERIFY_METHODS = [ "cmd", "test", @@ -91,6 +92,7 @@ var KNOWN_FIELDS = /* @__PURE__ */ new Set([ ...REPEATABLE, "kind", "risk", + "urgency", "confidence", "id" ]); @@ -350,9 +352,9 @@ function parseFrontmatter(source, diagnostics) { } if (data.review && typeof data.review === "object") { const r = data.review; - const status = typeof r.status === "string" ? r.status : "pending"; + const status2 = typeof r.status === "string" ? r.status : "pending"; meta.review = { - status: status === "approved" || status === "changes-requested" ? status : "pending", + status: status2 === "approved" || status2 === "changes-requested" ? status2 : "pending", by: typeof r.by === "string" ? r.by : void 0, at: typeof r.at === "string" ? r.at : void 0 }; @@ -670,6 +672,13 @@ function parseNoteHeader(raw, lineNo, filePath, counter, usedIds, diagnostics) { file: filePath ?? void 0 }); } + const urgency = URGENCIES.includes(attrs.urgency) ? attrs.urgency : void 0; + if (attrs.urgency && !urgency) diagnostics.push({ + level: "warning", + code: "urgency-unknown", + message: `Unknown urgency ${attrs.urgency}; preserved as a field.`, + line: lineNo + }); let confidence; if (attrs.confidence) { const value = Number(attrs.confidence.replace("%", "")); @@ -704,6 +713,7 @@ function parseNoteHeader(raw, lineNo, filePath, counter, usedIds, diagnostics) { selector, kind, risk, + urgency, confidence, sources: [], verify: [], @@ -712,7 +722,7 @@ function parseNoteHeader(raw, lineNo, filePath, counter, usedIds, diagnostics) { questions: [], refs: [], body: "", - extra: {}, + extra: attrs.urgency && !urgency ? { urgency: [attrs.urgency] } : {}, sourceLine: lineNo, file: filePath }; @@ -785,6 +795,14 @@ function setField(ctx, name, value, lineNo, diagnostics) { return; } } + if (key === "urgency") { + if (URGENCIES.includes(value)) { + note.urgency = value; + ctx.lastField = null; + return; + } + diagnostics.push({ level: "warning", code: "urgency-unknown", message: `Unknown urgency ${value}; preserved as a field.`, line: lineNo, noteId: note.id }); + } if (key === "kind" || key === "risk" || key === "confidence" || key === "id") { const num = Number(value.replace("%", "")); if (key === "kind" && NOTE_KINDS.includes(value)) { @@ -834,29 +852,29 @@ function appendToField(ctx, text) { const note = ctx.note; const field = ctx.lastField; if (!field) return; - const join3 = (existing) => existing ? `${existing} ${text}` : text; - if (field.name === "why") note.why = join3(note.why); - else if (field.name === "what") note.what = join3(note.what); - else if (field.name === "impact") note.impact = join3(note.impact); + const join6 = (existing) => existing ? `${existing} ${text}` : text; + if (field.name === "why") note.why = join6(note.why); + else if (field.name === "what") note.what = join6(note.what); + else if (field.name === "impact") note.impact = join6(note.impact); else if (field.name === "source") { const ref = note.sources[field.index]; - ref.note = join3(ref.note); + ref.note = join6(ref.note); ref.raw = `${ref.raw} ${text}`; } else if (field.name === "verify") { const claim = note.verify[field.index]; - claim.comment = join3(claim.comment); + claim.comment = join6(claim.comment); claim.raw = `${claim.raw} ${text}`; } else if (field.name === "alt") { - note.alternatives[field.index] = join3(note.alternatives[field.index]); + note.alternatives[field.index] = join6(note.alternatives[field.index]); } else if (field.name === "todo") { - note.todos[field.index] = join3(note.todos[field.index]); + note.todos[field.index] = join6(note.todos[field.index]); } else if (field.name === "question") { - note.questions[field.index] = join3(note.questions[field.index]); + note.questions[field.index] = join6(note.questions[field.index]); } else if (field.name === "ref") { - note.refs[field.index] = join3(note.refs[field.index]); + note.refs[field.index] = join6(note.refs[field.index]); } else if (field.name.startsWith("extra:")) { const key = field.name.slice("extra:".length); - note.extra[key][field.index] = join3(note.extra[key][field.index]); + note.extra[key][field.index] = join6(note.extra[key][field.index]); } } function finishNote(ctx, diagnostics) { @@ -921,7 +939,7 @@ function parseVerifyClaim(raw) { } else if (/^`/.test(rest)) { method = "cmd"; } - let status; + let status2; let comment; const arrow = rest.split(ARROW_RE); if (arrow.length > 1) { @@ -931,7 +949,7 @@ function parseVerifyClaim(raw) { tail ); if (statusMatch) { - status = normaliseStatus(statusMatch[1]); + status2 = normaliseStatus(statusMatch[1]); const after = tail.slice(statusMatch[0].length).trim(); comment = stripParens(after) || void 0; } else { @@ -940,7 +958,7 @@ function parseVerifyClaim(raw) { } else { const trailing = /\((pass|fail|unknown|skipped)\)\s*$/i.exec(rest); if (trailing) { - status = normaliseStatus(trailing[1]); + status2 = normaliseStatus(trailing[1]); rest = rest.slice(0, trailing.index).trim(); } } @@ -951,11 +969,11 @@ function parseVerifyClaim(raw) { detail = detail.slice(0, paren.index).trim(); } detail = detail.replace(/^`+|`+$/g, "").replace(/^"|"$/g, "").trim(); - if (!status) status = method === "none" ? "unknown" : "unknown"; + if (!status2) status2 = method === "none" ? "unknown" : "unknown"; return { method, detail: detail || void 0, - status, + status: status2, comment, raw: value }; @@ -1109,6 +1127,7 @@ function serializeNote(note, wrap = 78) { if (!/^n\d+$/.test(note.id)) head.push(`id=${note.id}`); const lines = [head.join(" ")]; const field = (name, value) => lines.push(...wrapField(name, value, wrap)); + if (note.urgency) field("urgency", note.urgency); if (note.why) field("why", note.why); if (note.what) field("what", note.what); for (const source of note.sources) field("source", source.raw); @@ -1565,7 +1584,7 @@ function coveredLines(file) { return { covered, verified }; } function hasPassingVerify(note) { - return note.verify.some((v) => v.status === "pass" && v.method !== "none"); + return !note.verify.some((v) => v.status === "fail") && note.verify.some((v) => v.status === "pass" && v.method !== "none"); } function addedLineNumbers(file) { const out = []; @@ -1660,8 +1679,67 @@ function percent(value) { } // src/lib/whymark/validate-tree.ts -import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { execFileSync as execFileSync2 } from "node:child_process"; +import { join, relative, resolve, sep } from "node:path"; + +// src/lib/whymark/evidence.ts +var hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value); +var nullableString = (value) => value === null || typeof value === "string"; +var status = (value) => ["pass", "fail", "unknown", "skipped"].includes(String(value)); +function readEvidence(value) { + if (!Array.isArray(value) || value.length > 1e3) return []; + return value.filter((r) => r && typeof r === "object" && r.version === 1 && typeof r.command === "string" && r.command.length <= 1e4 && nullableString(r.noteId) && nullableString(r.file) && status(r.claimed) && ["pass", "fail", "unavailable"].includes(r.status) && typeof r.ran === "string" && Number.isFinite(Date.parse(r.ran)) && Number.isFinite(r.durationMs) && r.durationMs >= 0 && (r.exitCode === null || Number.isInteger(r.exitCode)) && (r.status !== "pass" || r.exitCode === 0) && typeof r.cwd === "string" && typeof r.tool === "string" && typeof r.runtime === "string" && (r.source === null || hash(r.source)) && (r.sourceAfter === null || hash(r.sourceAfter)) && nullableString(r.head) && nullableString(r.base) && hash(r.reviewHash) && hash(r.outputHash) && typeof r.outputArtifact === "string" && typeof r.summary === "string"); +} + +// src/lib/quality/import.ts +var MAX_BYTES = 8 * 1024 * 1024; +function parseBoundedJson(text) { + if (new TextEncoder().encode(text).length > MAX_BYTES) throw new Error("Quality report exceeds 8 MB."); + return JSON.parse(text); +} +function safePath(path) { + return path.length > 0 && !path.startsWith("/") && !/^[A-Za-z]:/.test(path) && !path.includes("\\") && !path.split("/").includes("..") && !/[\0-\x1f]/.test(path); +} +function readQuality(value) { + if (!value || typeof value !== "object") return null; + const r = value; + const hash2 = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v); + const nullableHash = (v) => v === null || hash2(v); + if (r.version !== 1 || typeof r.clean !== "boolean" || typeof r.ran !== "string" || !Number.isFinite(Date.parse(r.ran)) || !nullableHash(r.source) || !nullableHash(r.sourceAfter) || !hash2(r.reviewHash) || !(r.head === null || typeof r.head === "string") || !(r.base === null || typeof r.base === "string") || !["local-run", "imported"].includes(r.provenance) || !["available", "unavailable"].includes(r.comparison) || !Array.isArray(r.checks) || r.checks.length > 100 || !Array.isArray(r.findings) || r.findings.length > 2e4) return null; + if (!r.checks.every((c2) => c2 && typeof c2.id === "string" && typeof c2.version === "string" && typeof c2.command === "string" && ["pass", "fail", "unavailable"].includes(c2.status) && (c2.exitCode === null || Number.isInteger(c2.exitCode)) && hash2(c2.outputHash) && typeof c2.outputArtifact === "string" && typeof c2.detail === "string")) return null; + if (!r.findings.every((f) => f && typeof f.id === "string" && typeof f.tool === "string" && typeof f.rule === "string" && typeof f.message === "string" && ["warning", "error"].includes(f.severity) && (f.path === null || typeof f.path === "string" && safePath(f.path)) && (f.line === null || Number.isInteger(f.line) && f.line > 0) && (f.endLine === null || Number.isInteger(f.endLine) && f.endLine >= (f.line ?? 1)) && (f.helpUrl === void 0 || typeof f.helpUrl === "string" && /^https?:\/\//.test(f.helpUrl)) && ["new", "existing", "resolved", "uncompared"].includes(f.status) && (f.suppression === void 0 || f.suppression !== null && typeof f.suppression === "object" && typeof f.suppression.reason === "string" && !!f.suppression.reason.trim() && typeof f.suppression.expires === "string" && Number.isFinite(Date.parse(f.suppression.expires))))) return null; + return r; +} +function parseESLint(text, relativePath) { + const input = parseBoundedJson(text); + if (!Array.isArray(input) || input.length > 2e4) throw new Error("Expected an ESLint JSON array."); + const findings = []; + for (const file of input) { + if (!file || typeof file.filePath !== "string" || !Array.isArray(file.messages)) throw new Error("Malformed ESLint file result."); + const path = relativePath(file.filePath); + if (!safePath(path)) throw new Error("ESLint result path escapes the repository."); + for (const message of file.messages) { + if (!message || typeof message.message !== "string" || ![1, 2].includes(message.severity) || !(message.ruleId === null || typeof message.ruleId === "string")) throw new Error("Malformed ESLint message."); + if (message.line !== void 0 && (!Number.isInteger(message.line) || message.line < 1)) throw new Error("Invalid finding line."); + if (message.endLine !== void 0 && (!Number.isInteger(message.endLine) || message.endLine < (message.line ?? 1))) throw new Error("Invalid finding range."); + findings.push({ + id: "", + tool: "eslint", + rule: message.ruleId ?? "parse-error", + severity: message.severity === 2 ? "error" : "warning", + message: message.message, + path, + line: message.line ?? null, + endLine: message.endLine ?? message.line ?? null, + status: "uncompared", + ...message.ruleId && /^[a-z-]+$/.test(message.ruleId) ? { helpUrl: `https://eslint.org/docs/latest/rules/${message.ruleId}` } : {} + }); + if (findings.length > 2e4) throw new Error("Too many quality findings."); + } + } + return findings; +} // src/lib/whymark/validate.ts function validateDocument(doc, options = {}) { @@ -1677,6 +1755,12 @@ function validateDocument(doc, options = {}) { message: "Document contains no file sections and no notes." }); } + if (doc.meta.extra.evidence !== void 0 && (!Array.isArray(doc.meta.extra.evidence) || readEvidence(doc.meta.extra.evidence).length !== doc.meta.extra.evidence.length)) { + diagnostics.push({ level: "warning", code: "evidence-invalid", message: "Malformed execution evidence was preserved but cannot establish verification." }); + } + if (doc.meta.extra.quality !== void 0 && !readQuality(doc.meta.extra.quality)) { + diagnostics.push({ level: "warning", code: "quality-invalid", message: "Malformed quality report was preserved but cannot establish a clean scan." }); + } if (!doc.meta.summary?.trim()) { diagnostics.push({ level: "warning", @@ -1830,8 +1914,140 @@ function collectStaleness(doc, cwd) { } function validateDocumentInRepo(doc, options = {}) { const extra = options.skipStaleness ? [] : collectStaleness(doc, options.cwd ?? process.cwd()); + if (!options.skipStaleness) extra.push(...collectSourceDiagnostics(doc, options.cwd ?? process.cwd())); return validateDocument(doc, { ...options, extraDiagnostics: extra }); } +function collectSourceDiagnostics(doc, cwd) { + const diagnostics = []; + for (const note of allNotes(doc)) for (const source of note.sources) { + let problem = null; + try { + if (source.type === "file") { + const match = source.locator.match(/^(.*?)(?::(\d+)(?:-(\d+))?)?$/); + if (!match || /[*?]/.test(match[1])) continue; + const path = match[1]; + if (path.startsWith("/") || path.split(/[\\/]/).includes("..")) throw new Error("reference escapes repository"); + let content; + const revision = doc.meta.scope === "commit" ? doc.meta.head?.match(/[a-f0-9]{7,40}$/)?.[0] : void 0; + if (doc.meta.scope === "commit" && !revision) throw new Error("recorded commit unavailable"); + if (revision) content = execFileSync2("git", ["show", `${revision}:${path}`], { cwd, encoding: "utf8", maxBuffer: 2 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }); + else { + const root = realpathSync(cwd); + const absolute = realpathSync(resolve(root, path)); + const rel = relative(root, absolute); + if (rel === ".." || rel.startsWith(`..${sep}`)) throw new Error("reference escapes repository"); + content = readFileSync(absolute, "utf8"); + } + const count = content.replace(/\n$/, "").split("\n").length; + if (match[2] && (Number(match[2]) < 1 || Number(match[3] ?? match[2]) > count || Number(match[3] ?? match[2]) < Number(match[2]))) problem = "line range is outside the file"; + } else if (source.type === "commit") { + if (!/^[a-f0-9]{7,40}$/.test(source.locator)) throw new Error("expected a commit hash"); + execFileSync2("git", ["cat-file", "-e", `${source.locator}^{commit}`], { cwd, stdio: "ignore" }); + } else if (source.type === "dep") { + const match = source.locator.match(/^(@?[^@]+)@(.+)$/); + if (!match || !/^(@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/i.test(match[1])) throw new Error("expected dependency@version"); + const pkg = JSON.parse(readFileSync(join(cwd, "node_modules", match[1], "package.json"), "utf8")); + if (pkg.version !== match[2]) problem = "installed dependency version differs from the citation"; + } + } catch { + problem = "reference unavailable at the reviewed location"; + } + if (problem) diagnostics.push({ level: "warning", code: "source-unavailable", message: `${source.locator}: ${problem}. Locator checks do not establish that a source supports the claim.`, noteId: note.id, file: note.file ?? void 0 }); + } + return diagnostics; +} + +// src/lib/whymark/evidence-node.ts +import { createHash } from "node:crypto"; +import { execFileSync as execFileSync3 } from "node:child_process"; +import { lstatSync, readFileSync as readFileSync2, readlinkSync, realpathSync as realpathSync2, mkdirSync, writeFileSync } from "node:fs"; +import { join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path"; +var sha256 = (text) => createHash("sha256").update(text).digest("hex"); +function gitHead(cwd) { + try { + return execFileSync3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); + } catch { + return null; + } +} +function excluded(path) { + return path.startsWith("reviews/") && (path.endsWith(".whymark") || path.endsWith(".quality.json")) || path.startsWith(".artifacts/"); +} +function cleanSource(cwd) { + try { + const entries = execFileSync3("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd, encoding: "utf8" }).split("\0").filter(Boolean); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (!excluded(entry.slice(3))) return false; + if (/[RC]/.test(entry.slice(0, 2)) && !excluded(entries[++i] ?? "")) return false; + } + return true; + } catch { + return false; + } +} +function resolveRevision(cwd, value) { + const hash2 = value?.match(/(?:^|@)([a-f0-9]{7,40})$/)?.[1]; + if (!hash2) return null; + try { + return execFileSync3("git", ["rev-parse", "--verify", `${hash2}^{commit}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); + } catch { + return null; + } +} +function sourceFingerprint(cwd) { + try { + const names = execFileSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); + const paths = [...new Set(names.split("\0").filter(Boolean))].sort(); + if (paths.length > 5e4) return null; + const digest = createHash("sha256").update("whymark-source-v1\0").update(gitHead(cwd) ?? "unborn"); + let bytes = 0; + for (const path of paths) { + if (excluded(path)) continue; + digest.update(`\0${path}\0`); + try { + const info = lstatSync(join2(cwd, path)); + digest.update(String(info.mode)); + if (info.isSymbolicLink()) digest.update(`link:${readlinkSync(join2(cwd, path))}`); + else if (info.isFile()) { + bytes += info.size; + if (info.size > 32 * 1024 * 1024 || bytes > 128 * 1024 * 1024) return null; + digest.update(readFileSync2(join2(cwd, path))); + } else return null; + } catch (error) { + if (error.code !== "ENOENT") return null; + digest.update("missing"); + } + } + return digest.digest("hex"); + } catch { + return null; + } +} +function reviewFingerprint(doc) { + return sha256(JSON.stringify({ base: doc.meta.base, head: doc.meta.head, files: doc.files.map((file) => ({ path: file.path, oldSha: file.oldSha, newSha: file.newSha, hunks: file.hunks.map((hunk) => ({ oldStart: hunk.oldStart, newStart: hunk.newStart, heading: hunk.heading, lines: hunk.lines })) })) })); +} +function writeOutput(cwd, output) { + const outputHash = sha256(output); + const outputArtifact = `.artifacts/whymark/${outputHash}.log`; + const root = realpathSync2(cwd); + const directory = join2(root, ".artifacts/whymark"); + for (const part of [join2(root, ".artifacts"), directory]) { + try { + if (lstatSync(part).isSymbolicLink()) throw new Error("Evidence artifact directory is a symlink."); + } catch (error) { + if (error.code !== "ENOENT") throw error; + } + } + mkdirSync(directory, { recursive: true }); + const path = join2(root, outputArtifact); + try { + writeFileSync(path, output, { flag: "wx" }); + } catch (error) { + if (error.code !== "EEXIST" || lstatSync(path).isSymbolicLink() || sha256(readFileSync2(path)) !== outputHash) throw error; + } + return { outputHash, outputArtifact }; +} // src/lib/whymark/verify.ts import { spawnSync as spawnSync2 } from "node:child_process"; @@ -1852,11 +2068,15 @@ function runCommand(cmd, options = {}) { status: result.status === 0 ? "pass" : "fail", durationMs: Date.now() - started, output, + unavailable: Boolean(result.error || result.status === null || result.status === 127), timedOut: Boolean(result.error && /ETIMEDOUT|timed out/i.test(String(result.error))) }; } function verifyDocument(doc, options = {}) { const results = []; + const cwd = options.cwd ?? process.cwd(); + const before = options.recordEvidence ? sourceFingerprint(cwd) : null; + const recordedHead = options.recordEvidence ? gitHead(cwd) : null; const cache = /* @__PURE__ */ new Map(); const run = (cmd) => { const cached = cache.get(cmd); @@ -1874,7 +2094,7 @@ function verifyDocument(doc, options = {}) { file: null, claimed: check.status, cmd: check.cmd, - outcome: outcomeFor(check.status, result.status), + outcome: result.unavailable ? "unrunnable" : outcomeFor(check.status, result.status), run: result }; applyToCheck(check, result); @@ -1906,7 +2126,7 @@ function verifyDocument(doc, options = {}) { file: note.file, claimed: claim.status, cmd, - outcome: outcomeFor(claim.status, result.status), + outcome: result.unavailable ? "unrunnable" : outcomeFor(claim.status, result.status), run: result }; applyToClaim(claim, result); @@ -1914,6 +2134,35 @@ function verifyDocument(doc, options = {}) { options.onFinish?.(claimResult); } } + if (options.recordEvidence) { + const after = sourceFingerprint(cwd); + const previous = readEvidence(doc.meta.extra.evidence); + const additions = results.filter((result) => result.run).map((result) => { + const run2 = result.run; + return { + version: 1, + command: result.cmd, + noteId: result.noteId, + file: result.file, + claimed: result.claimed, + status: run2.unavailable ? "unavailable" : run2.status, + ran: (/* @__PURE__ */ new Date()).toISOString(), + durationMs: run2.durationMs, + exitCode: run2.exitCode, + cwd: ".", + tool: `whymark@${options.toolVersion ?? "development"}`, + runtime: process.version, + source: before, + sourceAfter: after, + head: recordedHead, + base: doc.meta.base ?? null, + reviewHash: reviewFingerprint(doc), + ...writeOutput(cwd, run2.output), + summary: summarise(run2) + }; + }); + doc.meta.extra.evidence = [...previous.filter((old) => !additions.some((next) => next.command === old.command && next.noteId === old.noteId && next.file === old.file)), ...additions]; + } return results; } function commandOf(claim) { @@ -1962,8 +2211,341 @@ function summariseResults(results) { }; } +// src/cli/quality.ts +import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs"; +import { join as join3, relative as relative4, resolve as resolve4 } from "node:path"; + +// src/lib/quality/baseline.ts +function identifyFindings(findings) { + const counts = /* @__PURE__ */ new Map(); + return findings.map((finding) => { + const key = JSON.stringify([finding.tool, finding.rule, finding.path, finding.message]); + const occurrence = counts.get(key) ?? 0; + counts.set(key, occurrence + 1); + return { ...finding, id: sha256(`${key}:${occurrence}`) }; + }); +} +function compareFindings(current, baseline, renames = /* @__PURE__ */ new Map()) { + const usable = baseline && baseline.clean && current.base && baseline.head && current.base.endsWith(baseline.head) && baseline.source === baseline.sourceAfter && baseline.source !== null && current.source === current.sourceAfter && current.source !== null && current.checks.every((check) => check.status !== "unavailable" && baseline.checks.some((old2) => old2.id === check.id && old2.version === check.version && old2.command === check.command && old2.status !== "unavailable")); + if (!usable) return { ...current, comparison: "unavailable", findings: identifyFindings(current.findings).map((finding) => ({ ...finding, status: "uncompared" })) }; + const old = identifyFindings(baseline.findings.filter((finding) => finding.status !== "resolved").map((finding) => ({ ...finding, path: finding.path ? renames.get(finding.path) ?? finding.path : null }))); + const pending = new Map(old.map((finding) => [finding.id, finding])); + const findings = identifyFindings(current.findings).map((finding) => { + const existing = pending.delete(finding.id); + return { ...finding, status: existing ? "existing" : "new" }; + }); + for (const finding of pending.values()) { + if (current.checks.some((check) => check.id === finding.tool)) findings.push({ ...finding, status: "resolved" }); + } + return { ...current, comparison: "available", findings }; +} +function applySuppressions(findings, suppressions, now = Date.now()) { + return findings.map((finding) => { + const match = suppressions.find((item) => item.id === finding.id && item.reason.trim() && Date.parse(item.expires) > now); + const clean = { ...finding }; + delete clean.suppression; + return match ? { ...clean, suppression: { reason: match.reason, expires: match.expires } } : clean; + }); +} + +// src/lib/quality/run.ts +import { spawn } from "node:child_process"; +import { execFileSync as execFileSync4 } from "node:child_process"; +import { readFileSync as readFileSync3, realpathSync as realpathSync3 } from "node:fs"; +import { relative as relative3, resolve as resolve3 } from "node:path"; +function readConfig(path) { + const value = parseBoundedJson(readFileSync3(path, "utf8")); + if (!value || value.version !== 1 || !Array.isArray(value.checks) || !value.checks.length || value.checks.length > 20 || !value.checks.every((c2) => c2 && typeof c2.id === "string" && /^[a-z][a-z0-9-]*$/.test(c2.id) && typeof c2.command === "string" && c2.command.length > 0 && c2.command.length < 1e4 && typeof c2.versionCommand === "string" && c2.versionCommand.length > 0 && c2.versionCommand.length < 1e4 && ["eslint", "check"].includes(c2.format)) || new Set(value.checks.map((c2) => c2.id)).size !== value.checks.length) throw new Error("Invalid whymark.config.json checks."); + if (value.suppressions !== void 0 && (!Array.isArray(value.suppressions) || !value.suppressions.every((s) => s && typeof s.id === "string" && typeof s.reason === "string" && s.reason.trim() && typeof s.expires === "string" && Number.isFinite(Date.parse(s.expires))))) throw new Error("Invalid quality suppressions."); + return value; +} +function runTool(command, cwd, signal, timeoutMs = 12e4) { + return new Promise((resolveResult) => { + if (signal?.aborted) { + resolveResult({ code: null, stdout: "", stderr: "Cancelled", unavailable: true }); + return; + } + const child = spawn(command, { cwd, shell: true, detached: process.platform !== "win32", env: { ...process.env, CI: "1", NO_COLOR: "1" }, stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + let size = 0; + let unavailable = false; + const stop = (reason) => { + unavailable = true; + stderr += ` +${reason}`; + try { + if (child.pid && process.platform !== "win32") process.kill(-child.pid, "SIGKILL"); + else child.kill("SIGKILL"); + } catch { + } + }; + const abort = () => stop("Cancelled"); + signal?.addEventListener("abort", abort, { once: true }); + const timeout = setTimeout(() => stop("Timed out"), timeoutMs); + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + const append = (text, error) => { + size += Buffer.byteLength(text); + if (size > 8 * 1024 * 1024) { + if (!unavailable) stop("Output exceeds 8 MB"); + return; + } + if (error) stderr += text; + else stdout += text; + }; + child.stdout.on("data", (text) => append(text, false)); + child.stderr.on("data", (text) => append(text, true)); + child.on("error", (error) => { + unavailable = true; + stderr += error.message; + }); + child.on("close", (code) => { + clearTimeout(timeout); + signal?.removeEventListener("abort", abort); + resolveResult({ code, stdout, stderr, unavailable: unavailable || code === null || code === 127 }); + }); + }); +} +async function scanQuality(doc, cwd, config, options = {}) { + cwd = realpathSync3(cwd); + const report = { + version: 1, + ran: (/* @__PURE__ */ new Date()).toISOString(), + source: sourceFingerprint(cwd), + sourceAfter: null, + head: gitHead(cwd), + clean: cleanSource(cwd), + base: resolveRevision(cwd, doc.meta.base), + reviewHash: reviewFingerprint(doc), + provenance: "local-run", + comparison: "unavailable", + checks: [], + findings: [] + }; + for (const check of config.checks) { + if (options.signal?.aborted) break; + const version = await runTool(check.versionCommand, cwd, options.signal, 1e4); + const run = await runTool(check.command, cwd, options.signal); + let unavailable = run.unavailable || version.unavailable || version.code !== 0; + let detail = run.stderr.trim().slice(0, 1e3); + if (check.format === "eslint" && !unavailable) { + if (run.code !== 0 && run.code !== 1) unavailable = true; + else try { + const findings = parseESLint(run.stdout, (path) => relative3(cwd, resolve3(cwd, path)).split("\\").join("/")); + if (run.code === 1 && !findings.length) throw new Error("ESLint failed without findings."); + report.findings.push(...findings.map((finding) => ({ ...finding, tool: check.id }))); + } catch (error) { + unavailable = true; + detail = error.message; + } + } + report.checks.push({ + id: check.id, + version: version.stdout.trim().slice(0, 200) || "unavailable", + command: check.command, + status: unavailable ? "unavailable" : run.code === 0 ? "pass" : "fail", + exitCode: run.code, + ...writeOutput(cwd, `${run.stdout} +${run.stderr}`), + detail + }); + } + report.sourceAfter = sourceFingerprint(cwd); + const renames = /* @__PURE__ */ new Map(); + const base = options.baseline?.head; + if (base && /^[a-f0-9]{40}$/.test(base)) { + try { + const tokens = execFileSync4("git", ["diff", "--name-status", "-z", "--find-renames", base, "--"], { cwd, encoding: "utf8" }).split("\0"); + for (let i = 0; i < tokens.length; ) { + const status2 = tokens[i++]; + const old = tokens[i++]; + if (status2?.startsWith("R")) renames.set(old, tokens[i++]); + } + } catch { + } + } + const compared = compareFindings(report, options.baseline ?? null, renames); + compared.findings = applySuppressions(compared.findings, config.suppressions ?? []); + return compared; +} + +// src/cli/quality.ts +async function qualityCommand(positionals, options) { + const cwd = repoRoot() || process.cwd(); + if (positionals[0] === "init") { + const path2 = join3(cwd, "whymark.config.json"); + writeFileSync2(path2, `${JSON.stringify({ version: 1, checks: [ + { id: "eslint", format: "eslint", command: "node node_modules/eslint/bin/eslint.js . --format json --rule 'complexity: [warn, 15]' --rule 'max-depth: [warn, 4]'", versionCommand: "node node_modules/eslint/bin/eslint.js --version" }, + { id: "typecheck", format: "check", command: "npm run typecheck", versionCommand: "node node_modules/typescript/bin/tsc --version" } + ], suppressions: [] }, null, 2)} +`, { flag: "wx" }); + process.stdout.write(`Created ${path2}. Review the commands before running quality --run. +`); + return; + } + const path = positionals[0]; + if (!path || (options.run ? 1 : 0) + (options.importPath ? 1 : 0) !== 1 || options.watch && !options.run) { + throw new Error("Use quality --run or --import . See whymark help quality."); + } + let baseline = null; + if (options.baseline) { + baseline = readQuality(parseBoundedJson(readFileSync4(options.baseline, "utf8"))); + if (!baseline) throw new Error("Invalid baseline report."); + } + let controller = new AbortController(); + let generation = 0; + const execute = async () => { + const mine = ++generation; + controller.abort(); + controller = new AbortController(); + const signal = controller.signal; + const text = readFileSync4(path, "utf8"); + const doc = parseWhymark(text); + let report; + if (options.importPath) { + const input = readFileSync4(options.importPath, "utf8"); + report = compareFindings({ + version: 1, + ran: (/* @__PURE__ */ new Date()).toISOString(), + source: null, + sourceAfter: null, + head: gitHead(cwd), + clean: false, + base: doc.meta.base ?? null, + reviewHash: reviewFingerprint(doc), + provenance: "imported", + comparison: "unavailable", + checks: [{ id: "eslint", version: options.toolVersion ?? "unknown", command: "Imported ESLint JSON (not executed)", status: "unavailable", exitCode: null, ...writeOutput(cwd, input), detail: "Execution and source state were not independently checked." }], + findings: parseESLint(input, (name) => relative4(cwd, resolve4(cwd, name)).split("\\").join("/")) + }, null); + } else report = await scanQuality(doc, cwd, readConfig(options.config ?? join3(cwd, "whymark.config.json")), { baseline, signal }); + if (signal.aborted || mine !== generation) return; + if (options.write) { + if (readFileSync4(path, "utf8") !== text) throw new Error("Review changed during the scan; results were not attached. Rerun against the current review."); + doc.meta.extra.quality = report; + writeFileSync2(path, serializeWhymark(doc)); + } + process.stdout.write(`${JSON.stringify(report, null, 2)} +`); + if (!options.watch) process.exitCode = report.checks.some((check) => check.status !== "pass") || report.findings.some((f) => f.severity === "error" && f.status !== "resolved" && !f.suppression) ? 1 : 0; + }; + if (!options.watch) { + await execute(); + return; + } + let previous = sourceFingerprint(cwd); + if (!previous) throw new Error("Cannot watch: repository fingerprint unavailable."); + let timer; + const launch = () => { + void execute().catch((error) => process.stderr.write(`${error.message} +`)); + }; + launch(); + const interval = setInterval(() => { + const next = sourceFingerprint(cwd); + if (next === previous) return; + previous = next; + controller.abort(); + generation++; + clearTimeout(timer); + timer = setTimeout(launch, 400); + }, 1e3); + await new Promise((done) => { + const stop = () => { + clearInterval(interval); + clearTimeout(timer); + controller.abort(); + generation++; + process.removeListener("SIGINT", stop); + process.removeListener("SIGTERM", stop); + done(); + }; + process.on("SIGINT", stop); + process.on("SIGTERM", stop); + }); +} + +// src/lib/skill/install.ts +import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, realpathSync as realpathSync4, writeFileSync as writeFileSync3 } from "node:fs"; +import { dirname, join as join4, relative as relative5, resolve as resolve5, sep as sep3 } from "node:path"; +import { homedir } from "node:os"; +function installSkill(options) { + const agents = [...new Set(options.agents?.length ? options.agents : ["codex"])]; + if (agents.some((agent) => !["codex", "claude-code"].includes(agent))) { + throw new Error("Supported agents: codex, claude-code."); + } + const base = realpathSync4(options.global ? options.home ?? homedir() : options.cwd); + const skill = readFileSync5(join4(options.packageRoot, ".agents/skills/whymark/SKILL.md"), "utf8").replaceAll("spec/whymark-v1.md", "references/whymark-v1.md"); + const assets = [ + ["SKILL.md", skill], + ["references/whymark-v1.md", readFileSync5(join4(options.packageRoot, "spec/whymark-v1.md"), "utf8")] + ]; + const pending = []; + for (const agent of agents) { + const destination = join4(base, agent === "codex" ? ".agents" : ".claude", "skills/whymark"); + for (const [name, content] of assets) { + const path = resolve5(destination, name); + assertNoSymlinks(base, path); + const identical = existsSync2(path) && readFileSync5(path, "utf8") === content; + if (!identical && existsSync2(path) && !options.force) { + throw new Error(`Refusing to overwrite ${path}. Use --force to replace this skill explicitly.`); + } + pending.push({ path, content, action: identical ? "unchanged" : existsSync2(path) ? "replace" : "create" }); + } + } + if (!options.dryRun) { + for (const file of pending) { + if (file.action === "unchanged") continue; + assertNoSymlinks(base, file.path); + mkdirSync2(dirname(file.path), { recursive: true }); + writeFileSync3(file.path, file.content, { flag: file.action === "create" ? "wx" : "w" }); + } + } + return pending.map(({ path, action }) => ({ path, action })); +} +function assertNoSymlinks(base, path) { + const parts = relative5(base, path).split(sep3); + if (parts.includes("..")) throw new Error("Skill destination escapes its installation directory."); + let current = base; + for (const [index, part] of parts.entries()) { + current = join4(current, part); + let info; + try { + info = lstatSync2(current); + } catch (error) { + if (error.code === "ENOENT") continue; + throw error; + } + if (info.isSymbolicLink()) throw new Error(`Refusing symlink destination ${current}.`); + if (index < parts.length - 1 ? !info.isDirectory() : !info.isFile()) { + throw new Error(`Unexpected destination type at ${current}.`); + } + } +} + // src/cli/help.ts var TOPICS = { + quality: { aliases: [], render: () => `npx whymark quality init +npx whymark quality --run [--write] [--baseline report.quality.json] +npx whymark quality --run --watch --write +npx whymark quality --import eslint.json --tool-version [--write] + +init writes whymark.config.json without overwriting it. Inspect its commands before +--run: configured commands execute locally. Opening a review never runs them. +--watch cancels superseded scans and debounces repository changes. Ctrl-C stops it. +--config selects trusted local configuration. --baseline compares a saved +report whose head matches the review base; otherwise findings stay uncompared. +JSON goes to stdout. --write also attaches results to the review. Imported ESLint +JSON is evidence supplied by its author, not a fresh execution. No AI is required. +` }, + skill: { aliases: [], render: () => `npx whymark skill install [--agent codex|claude-code] [--global] [--dry-run] [--force] + +Installs the bundled agent skill and format reference without Git access. +Default: project-local Codex skill. Repeat --agent to install both targets. +--dry-run previews all paths; identical files are left alone. --force explicitly +replaces customized skill files. No postinstall hooks or instruction-file edits. +` }, new: { aliases: ["init"], render: helpNew }, prompt: { aliases: [], render: helpPrompt }, validate: { aliases: ["check"], render: helpValidate }, @@ -2013,6 +2595,8 @@ ${c2.bold("WORKFLOW")} 5. open https://whymark.x47.dev drop the file; it stays in the browser ${c2.bold("COMMANDS")} + quality configured local checks and ESLint findings + skill install the bundled agent skill from npm new skeleton from a git diff ${c2.gray("alias: init")} prompt authoring prompt with the diff embedded validate structure, staleness, coverage ${c2.gray("alias: check")} @@ -2381,6 +2965,19 @@ function main() { return cmdHelp(isHelpToken(args.command) ? args.positionals[0] : args.command); } switch (args.command) { + case "quality": + return qualityCommand(args.positionals, { run: args.has("run"), watch: args.has("watch"), write: args.has("write"), config: args.str("config"), baseline: args.str("baseline"), importPath: args.str("import"), toolVersion: args.str("tool-version") }).catch((error) => fail(error.message)); + case "skill": { + if (args.positionals[0] !== "install") fail("Usage: npx whymark skill install [--agent codex|claude-code] [--global] [--dry-run] [--force]"); + try { + const result = installSkill({ packageRoot: packageRoot(), cwd: process.cwd(), agents: args.all("agent"), global: args.has("global"), dryRun: args.has("dry-run"), force: args.has("force") }); + for (const item of result) process.stdout.write(`${item.action} ${item.path} +`); + } catch (error) { + fail(error.message); + } + return; + } case "new": case "init": return cmdNew(args); @@ -2468,11 +3065,11 @@ function cmdNew(args) { process.stdout.write(text); return; } - const target = out ? resolve(cwd, out) : resolve(cwd, "reviews", `${slug(doc.meta.title)}.whymark`); - mkdirSync(dirname(target), { recursive: true }); - writeFileSync(target, text); + const target = out ? resolve6(cwd, out) : resolve6(cwd, "reviews", `${slug(doc.meta.title)}.whymark`); + mkdirSync3(dirname2(target), { recursive: true }); + writeFileSync4(target, text); const stats = computeStats(doc); - const rel = relative(cwd, target) || target; + const rel = relative6(cwd, target) || target; process.stdout.write( [ `${c.green("\u2713")} wrote ${c.bold(rel)}`, @@ -2502,15 +3099,15 @@ function cmdPrompt(args) { if (!diff.files.length) noChanges(doc.meta.scope); const skeleton = serializeWhymark(doc); const pack = packageRoot(); - const templatePath = [join2(cwd, "prompts", "whymark-author.md"), join2(pack, "prompts", "whymark-author.md")].find( - existsSync2 + const templatePath = [join5(cwd, "prompts", "whymark-author.md"), join5(pack, "prompts", "whymark-author.md")].find( + existsSync3 ); - const template = templatePath ? stripPreamble(readFileSync(templatePath, "utf8")) : FALLBACK_PROMPT; - const specInRepo = existsSync2(join2(cwd, "spec/whymark-v1.md")); + const template = templatePath ? stripPreamble(readFileSync6(templatePath, "utf8")) : FALLBACK_PROMPT; + const specInRepo = existsSync3(join5(cwd, "spec/whymark-v1.md")); process.stdout.write( template.replace("{{SKELETON}}", skeleton.trimEnd()).replace( "{{SPEC_PATH}}", - specInRepo ? "spec/whymark-v1.md" : "https://github.com/spink-dev/whymark/blob/main/spec/whymark-v1.md" + specInRepo ? "spec/whymark-v1.md" : join5(pack, "spec/whymark-v1.md") ) ); } @@ -2529,8 +3126,8 @@ command you ran. Do not narrate what the code does. Keep the diff bytes untouche \`\`\` `; function loadDoc(path) { - if (!existsSync2(path)) fail(`No such file: ${path}`); - const text = readFileSync(path, "utf8"); + if (!existsSync3(path)) fail(`No such file: ${path}`); + const text = readFileSync6(path, "utf8"); return parseWhymark(text, { filename: path }); } function cmdValidate(args) { @@ -2575,7 +3172,7 @@ function printValidation(path, result) { } } process.stdout.write( - ` ${bar(stats.coverage)} ${c.bold(percent(stats.coverage))} of ${stats.added} added lines annotated \xB7 ${percent(stats.verifiedCoverage)} verified \xB7 ${stats.notes} notes \xB7 ${stats.sourced} sourced / ${stats.inferenceOnly} inference-only + ` ${bar(stats.coverage)} ${c.bold(percent(stats.coverage))} of ${stats.added} added lines annotated \xB7 ${percent(stats.verifiedCoverage)} claimed verified \xB7 ${stats.notes} notes \xB7 ${stats.sourced} sourced / ${stats.inferenceOnly} inference-only ` ); const verdict = result.ok ? c.green("passes") : `${c.red("fails")} (${result.errors} error(s), ${result.warnings} warning(s))`; @@ -2599,6 +3196,8 @@ function cmdVerify(args) { const json = args.has("json"); const results = verifyDocument(doc, { cwd, + recordEvidence: args.has("write"), + toolVersion: pkgVersion(), filter: filter ? new RegExp(filter) : void 0, onStart: (cmd) => { if (!json) process.stdout.write(`${c.gray("\u2192 running")} ${cmd} @@ -2622,11 +3221,11 @@ ${c.bold("verified")} ${summary.total} claim(s): ${c.green(`${summary.confirmed} ); } if (args.has("write")) { - writeFileSync(path, serializeWhymark(doc)); + writeFileSync4(path, serializeWhymark(doc)); if (!json) process.stdout.write(`${c.green("\u2713")} updated ${path} with real results `); } - process.exit(summary.contradicted > 0 ? 1 : 0); + process.exit(summary.contradicted > 0 || summary.unrunnable > 0 || results.some((result) => result.run?.status === "fail") ? 1 : 0); } function outcomeLabel(result) { const where = result.noteId ? c.gray(` [${result.noteId}]`) : c.gray(" [check]"); @@ -2701,7 +3300,7 @@ function cmdFmt(args) { const doc = loadDoc(path); const text = serializeWhymark(doc); if (args.has("write")) { - writeFileSync(path, text); + writeFileSync4(path, text); process.stdout.write(`${c.green("\u2713")} formatted ${path} `); } else { @@ -2715,20 +3314,20 @@ function slug(title) { return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "review"; } function packageRoot() { - const here = dirname(fileURLToPath(import.meta.url)); - for (const dir of [join2(here, ".."), join2(here, "../..")]) { - const pkgPath = join2(dir, "package.json"); - if (!existsSync2(pkgPath)) continue; + const here = dirname2(fileURLToPath(import.meta.url)); + for (const dir of [join5(here, ".."), join5(here, "../..")]) { + const pkgPath = join5(dir, "package.json"); + if (!existsSync3(pkgPath)) continue; try { - if (JSON.parse(readFileSync(pkgPath, "utf8")).name === "whymark") return dir; + if (JSON.parse(readFileSync6(pkgPath, "utf8")).name === "whymark") return dir; } catch { } } - return join2(here, "../.."); + return join5(here, "../.."); } function pkgVersion() { try { - return JSON.parse(readFileSync(join2(packageRoot(), "package.json"), "utf8")).version ?? "0.0.0"; + return JSON.parse(readFileSync6(join5(packageRoot(), "package.json"), "utf8")).version ?? "0.0.0"; } catch { return "0.0.0"; } @note +5..6 kind=generated id=improvement-7 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +29 kind=generated id=improvement-8 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +95 kind=generated id=improvement-9 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +355..357 kind=generated id=improvement-10 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +675..681 kind=generated id=improvement-11 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +716 kind=generated id=improvement-12 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +725 kind=generated id=improvement-13 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +798..805 kind=generated id=improvement-14 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +855..877 kind=generated id=improvement-15 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +942 kind=generated id=improvement-16 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +952 kind=generated id=improvement-17 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +961 kind=generated id=improvement-18 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +972..976 kind=generated id=improvement-19 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +1130 kind=generated id=improvement-20 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +1587 kind=generated id=improvement-21 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +1682..1742 kind=generated id=improvement-22 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +1758..1763 kind=generated id=improvement-23 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +1917..2050 kind=generated id=improvement-24 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2071..2079 kind=generated id=improvement-25 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2097 kind=generated id=improvement-26 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2129 kind=generated id=improvement-27 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2137..2165 kind=generated id=improvement-28 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2214..2548 kind=generated id=improvement-29 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2598..2599 kind=generated id=improvement-30 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +2968..2980 kind=generated id=improvement-31 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3068..3072 kind=generated id=improvement-32 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3102..3110 kind=generated id=improvement-33 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3129..3130 kind=generated id=improvement-34 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3175 kind=generated id=improvement-35 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3199..3200 kind=generated id=improvement-36 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3224..3228 kind=generated id=improvement-37 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3303 kind=generated id=improvement-38 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @note +3317..3330 kind=generated id=improvement-39 urgency: normal why: The shipped CLI must contain the same installer, format handling and evidence checks as the source, so npm consumers receive the tested behavior. source: file:package.json — build:cli defines the generated npm entrypoint verify: cmd `npm run build:cli` => pass (exit 0 in 0.1s) @file eslint.config.mjs modified +1 oldsha=05e726d newsha=b5d8c8f @@ -11,6 +11,7 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + "dist/**", "next-env.d.ts", ]), ]); @note +14 kind=intent id=improvement-40 urgency: normal why: The CLI bundle is generated from already-linted TypeScript; linting its tree-shaken output reports unused generated imports rather than actionable source defects. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run lint` => pass (exit 0 in 2.8s) @file package.json modified +6 -4 oldsha=45dc139 newsha=4a7dece @@ -1,7 +1,7 @@ { "name": "whymark", "version": "0.2.0", - "description": "whymark — a file format and reviewer for reading AI-written code changes with the reasoning, sources, and verification attached to each line.", + "description": "whymark \u2014 a file format and reviewer for reading AI-written code changes with the reasoning, sources, and verification attached to each line.", "author": "Samuel Spink", "license": "MIT", "bin": { @@ -13,7 +13,8 @@ "prompts/whymark-author.md", "spec/whymark-v1.md", "README.md", - "LICENSE" + "LICENSE", + ".agents/skills/whymark/SKILL.md" ], "engines": { "node": "22.x" @@ -36,7 +37,7 @@ "scripts": { "dev": "next dev --port 43917", "build": "next build", - "build:cli": "esbuild src/cli/whymark.ts --bundle --platform=node --format=esm --outfile=dist/whymark.mjs --external:yaml --legal-comments=none --banner:js='/* generated by npm run build:cli — do not edit */'", + "build:cli": "esbuild src/cli/whymark.ts --bundle --platform=node --format=esm --outfile=dist/whymark.mjs --external:yaml --legal-comments=none --banner:js='/* generated by npm run build:cli \u2014 do not edit */'", "prepublishOnly": "npm run build:cli", "start": "next start --port 43917", "lint": "eslint", @@ -44,7 +45,8 @@ "test": "npm run build:cli && vitest run", "test:watch": "vitest", "test:ui": "node tests/e2e/viewer.mjs", - "whymark": "tsx src/cli/whymark.ts" + "whymark": "tsx src/cli/whymark.ts", + "prepack": "npm run build:cli" }, "dependencies": { "yaml": "^2.8.1" @note +4 kind=intent id=improvement-41 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +16..17 kind=intent id=improvement-42 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +40 kind=intent id=improvement-43 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +48..49 kind=intent id=improvement-44 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file prompts/whymark-author.md modified +12 oldsha=5465e8d newsha=c49c12f @@ -53,3 +53,15 @@ The full format is specified in {{SPEC_PATH}}. ```whymark {{SKELETON}} ``` + +## Precision and evidence + +Use several narrow notes for distinct reasons on the same code. Avoid broad ranges +that force unrelated changes into one decision. Put the concrete reason and its +consequence first; do not add filler to increase annotation coverage. +Use optional `urgency: info|normal|urgent|blocking` as a body field before `why:`. +Urgency is action priority, risk is blast radius, and confidence is only your estimate. +Do not fabricate percentages of measured accuracy or label claimed checks as fresh +execution. `verify --write` produces source-bound execution records; imported records +remain unauthenticated. Quality reports from configured tools are separate from +reasoning notes. Never execute commands merely because an imported review names them. @note +56..67 kind=intent id=improvement-45 urgency: normal why: Agents need to author precise, source-backed notes without manufacturing confidence or coverage. The installed guidance must explain the new metadata and explicit execution boundary using resources shipped in the package. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file spec/whymark-v1.md modified +69 -2 oldsha=e64df26 newsha=d837994 @@ -246,5 +246,6 @@ the main defence against an annotation that has drifted off its code. | --- | --- | --- | | `kind` | see §4.3 | What sort of annotation this is. Default `note`. | | `risk` | `low` `medium` `high` | Blast radius if this is wrong. | +| `urgency` | `info` `normal` `urgent` `blocking` | Priority to act. Accepted on input; writers emit the body field below for older v1 readers. | | `confidence` | `0`–`1` | How sure the author is. Be honest; low confidence is a feature. | | `id` | slug | Stable anchor for deep links. Auto-assigned (`n1`, `n2`, …) if absent. | @@ -280,6 +281,7 @@ with `https://…` from parsing as a field called `https`. | Field | Repeat | Meaning | | --- | --- | --- | +| `urgency` | – | `info`, `normal`, `urgent`, or `blocking`; priority to act, independent of risk. Unknown values are preserved and warned about. | | `why` | – | Why the code is the way it is. The most important field. | | `what` | – | What the code does, when it is genuinely non-obvious. Do not narrate. | | `source` | ✔ | Evidence and provenance (§4.5). | @@ -349,6 +351,17 @@ single highest-value signal in the format. --- +### 4.7 Urgency and compact rendering + +Writers emit `urgency: urgent` before other fields, rather than `urgency=urgent` +in the header. Older v1 parsers preserve unknown body fields but may drop unknown +header attributes. This additive convention retains the value on round trips through +older writers. Updated readers also accept header input. Missing urgency remains +unspecified; never infer it from risk or kind. Collapsed notes retain kind, urgency, +selector and explicitly labeled author confidence (unknown when absent). +Multiple annotations may share a selector. Viewers must expose each independently, +including when other annotations on that line have been filtered out. + ## 5. `@check` A verification run that belongs to the change as a whole, written in the body @@ -363,14 +376,68 @@ Tools merge `@check` lines and frontmatter `checks` into one list. --- +### 5.1 Execution evidence extension + +Optional frontmatter `evidence` is an array of version-1 execution records. +Existing v1 tools preserve this frontmatter extension; a reader must not promote +legacy passing claims to fresh execution results. Each record contains: + +- `version: 1`, `command`, `noteId` (null for a global check), `file` (nullable), + `claimed` (original claim), `status` (`pass`, `fail`, `unavailable`). +- `ran` (ISO time), `durationMs`, `exitCode` (nullable), `cwd` (repo relative), + `tool` (whymark version), `runtime` (Node version). +- `source` / `sourceAfter` (nullable SHA-256 source fingerprints), `head` and + `base` (nullable revisions), `reviewHash` (SHA-256 of the normalized diff). +- `outputHash` (SHA-256), `outputArtifact` (repo-relative output file), `summary`. + +`verify --write` records these alongside updated claims. Logs are saved under +`.artifacts/whymark/.log`; inspect logs before sharing them. Fingerprints +cover HEAD, tracked files (including deletions), and non-ignored untracked files, +including tests/config/lockfiles and symlink targets as link text. Generated +`.artifacts/` and review outputs under `reviews/` (`.whymark`, `.quality.json`) +are excluded. Ignored inputs, installed dependency bytes and external services are +not fingerprinted; this is bounded evidence, not a reproducible-build attestation. +Reviews outside `reviews/` may invalidate their own source fingerprint when rewritten. + +Hashes bind evidence to bytes; they do not authenticate the author or guarantee +truth. Imported records are labeled as such. The local viewer checks current source, +normalized diff identity and output hashes before showing a current record. A changed +source or diff is stale; missing outputs/fingerprints are unavailable. A passing and +failing claim on one note cannot yield unqualified passing coverage. A contradiction +is retained even when the original claim is rewritten with the command result. +Viewing/importing never executes commands. Explicit `verify` executes shell commands +from the review; inspect them before using it on an untrusted document. + +### 5.2 Quality report extension + +Optional frontmatter `quality` holds one version-1 report. Fields: `version`, `ran`, +`source`, `sourceAfter`, `head`, `clean` (whether relevant source was clean), `base`, `reviewHash`, `provenance` (`local-run` or +`imported`), `comparison` (`available` or `unavailable`), `checks`, and `findings`. +Each check has `id`, `version`, `command`, `status` (`pass`, `fail`, `unavailable`), +`exitCode`, `outputHash`, `outputArtifact`, and `detail`. +Each finding has `id`, `tool`, `rule`, `severity` (`warning` or `error`), `message`, +nullable repo-relative `path`, nullable one-based `line` and `endLine`, optional +HTTP(S) `helpUrl`, and `status` (`new`, `existing`, `resolved`, `uncompared`). Optional +`suppression` contains `reason` and an ISO `expires` time. Expired suppressions do +not suppress a new scan. IDs derive from tool/rule/path/message plus duplicate +occurrence, so shifted lines retain identity; identical duplicate findings can be +ambiguous. Rename mappings are taken from Git when a baseline is available. + +Quality reports are supplied evidence, never measured accuracy. Failed/missing +scanners and missing/mismatched baselines cannot be treated as a clean comparison. +Importers bound report size, validate paths/ranges and preserve project-level findings. +Unknown extension data remains in frontmatter; malformed known data is diagnosed +and must not be interpreted as successful evidence. + ## 6. Derived metrics Tools compute these; they are never stored in the file. - **Coverage** — the share of added lines (`+`) covered by at least one annotation. Uncovered added lines are code that arrived with no explanation. -- **Verified coverage** — the share of added lines covered by an annotation with - at least one `verify` claim whose status is `pass`. +- **Claimed verified coverage** (API: `verifiedCoverage`) — the share of added lines covered by an annotation with + at least one `verify` claim whose status is `pass`, with no failing claim on that note. This is author-reported + coverage, not fresh execution or a correctness score. - **Sourced share** — annotations with at least one `source` that is not `inference`. - **Open items** — `todo` and `question` fields, plus `fail`/`unknown` claims. @note +249 kind=intent id=improvement-46 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +284 kind=intent id=improvement-47 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +354..364 kind=intent id=improvement-48 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +379..440 kind=intent id=improvement-49 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/app/r/[slug]/page.tsx modified +4 oldsha=e0fc84d newsha=5b1e684 @@ -1,3 +1,4 @@ +import { localEvidence, sourceFingerprint, reviewFingerprint } from "@/lib/whymark/evidence-node"; import { notFound } from "next/navigation"; import { loadReview, listReviews } from "@/lib/reviews"; import { validateDocumentInRepo } from "@/lib/whymark/validate-tree"; @@ -32,6 +33,9 @@ export default async function ReviewPage({ params }: PageProps<"/r/[slug]">) { const writable = canWriteWorkingTree(); const [vm, validation] = await Promise.all([ buildReviewVM(review.doc, { + source: writable ? sourceFingerprint(process.cwd()) : undefined, + reviewHash: reviewFingerprint(review.doc), + evidence: writable ? localEvidence(review.doc, process.cwd()) : undefined, isWritable: writable ? (file) => isActionable(file, hashObject(file.path, process.cwd())) : undefined, @note +1 kind=intent id=improvement-50 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +36..38 kind=intent id=improvement-51 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @file src/cli/help.ts modified +22 oldsha=826efdf newsha=ec0ff08 @@ -7,6 +7,26 @@ export type Palette = { }; const TOPICS: Record string }> = { + quality: { aliases: [], render: () => `npx whymark quality init +npx whymark quality --run [--write] [--baseline report.quality.json] +npx whymark quality --run --watch --write +npx whymark quality --import eslint.json --tool-version [--write] + +init writes whymark.config.json without overwriting it. Inspect its commands before +--run: configured commands execute locally. Opening a review never runs them. +--watch cancels superseded scans and debounces repository changes. Ctrl-C stops it. +--config selects trusted local configuration. --baseline compares a saved +report whose head matches the review base; otherwise findings stay uncompared. +JSON goes to stdout. --write also attaches results to the review. Imported ESLint +JSON is evidence supplied by its author, not a fresh execution. No AI is required. +` }, + skill: { aliases: [], render: () => `npx whymark skill install [--agent codex|claude-code] [--global] [--dry-run] [--force] + +Installs the bundled agent skill and format reference without Git access. +Default: project-local Codex skill. Repeat --agent to install both targets. +--dry-run previews all paths; identical files are left alone. --force explicitly +replaces customized skill files. No postinstall hooks or instruction-file edits. +` }, new: { aliases: ["init"], render: helpNew }, prompt: { aliases: [], render: helpPrompt }, validate: { aliases: ["check"], render: helpValidate }, @@ -62,6 +82,8 @@ ${c.bold("WORKFLOW")} 5. open https://whymark.x47.dev drop the file; it stays in the browser ${c.bold("COMMANDS")} + quality configured local checks and ESLint findings + skill install the bundled agent skill from npm new skeleton from a git diff ${c.gray("alias: init")} prompt authoring prompt with the diff embedded validate structure, staleness, coverage ${c.gray("alias: check")} @note +10..29 kind=intent id=improvement-52 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +85..86 kind=intent id=improvement-53 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/cli/whymark.ts modified +17 -3 oldsha=117a644 newsha=4079220 @@ -10,6 +10,8 @@ import { validateDocument } from "../lib/whymark/validate"; import { validateDocumentInRepo } from "../lib/whymark/validate-tree"; import { summariseResults, verifyDocument, type ClaimResult } from "../lib/whymark/verify"; import type { Scope } from "../lib/whymark/types"; +import { qualityCommand } from "./quality"; +import { installSkill } from "../lib/skill/install"; import { renderHelp } from "./help"; const c = colors(); @@ -116,6 +118,16 @@ function main() { return cmdHelp(isHelpToken(args.command) ? args.positionals[0] : args.command); } switch (args.command) { + case "quality": + return qualityCommand(args.positionals, { run: args.has("run"), watch: args.has("watch"), write: args.has("write"), config: args.str("config"), baseline: args.str("baseline"), importPath: args.str("import"), toolVersion: args.str("tool-version") }).catch(error => fail((error as Error).message)); + case "skill": { + if (args.positionals[0] !== "install") fail("Usage: npx whymark skill install [--agent codex|claude-code] [--global] [--dry-run] [--force]"); + try { + const result = installSkill({ packageRoot: packageRoot(), cwd: process.cwd(), agents: args.all("agent"), global: args.has("global"), dryRun: args.has("dry-run"), force: args.has("force") }); + for (const item of result) process.stdout.write(`${item.action} ${item.path}\n`); + } catch (error) { fail((error as Error).message); } + return; + } case "new": case "init": return cmdNew(args); @@ -265,7 +277,7 @@ function cmdPrompt(args: Args) { process.stdout.write( template.replace("{{SKELETON}}", skeleton.trimEnd()).replace( "{{SPEC_PATH}}", - specInRepo ? "spec/whymark-v1.md" : "https://github.com/spink-dev/whymark/blob/main/spec/whymark-v1.md", + specInRepo ? "spec/whymark-v1.md" : join(pack, "spec/whymark-v1.md"), ), ); } @@ -345,7 +357,7 @@ function printValidation( } process.stdout.write( ` ${bar(stats.coverage)} ${c.bold(percent(stats.coverage))} of ${stats.added} added lines annotated · ` + - `${percent(stats.verifiedCoverage)} verified · ${stats.notes} notes · ` + + `${percent(stats.verifiedCoverage)} claimed verified · ${stats.notes} notes · ` + `${stats.sourced} sourced / ${stats.inferenceOnly} inference-only\n`, ); const verdict = result.ok @@ -372,6 +384,8 @@ function cmdVerify(args: Args) { const results = verifyDocument(doc, { cwd, + recordEvidence: args.has("write"), + toolVersion: pkgVersion(), filter: filter ? new RegExp(filter) : undefined, onStart: (cmd) => { if (!json) process.stdout.write(`${c.gray("→ running")} ${cmd}\n`); @@ -399,7 +413,7 @@ function cmdVerify(args: Args) { if (!json) process.stdout.write(`${c.green("✓")} updated ${path} with real results\n`); } - process.exit(summary.contradicted > 0 ? 1 : 0); + process.exit(summary.contradicted > 0 || summary.unrunnable > 0 || results.some(result => result.run?.status === "fail") ? 1 : 0); } function outcomeLabel(result: ClaimResult): string { @note +13..14 kind=intent id=improvement-54 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +121..130 kind=intent id=improvement-55 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +280 kind=intent id=improvement-56 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +360 kind=intent id=improvement-57 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +387..388 kind=intent id=improvement-58 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +416 kind=intent id=improvement-59 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/components/whymark/file-panel.tsx modified +111 -90 oldsha=4b3b038 newsha=3381d13 @@ -8,6 +8,7 @@ import { useMemo, useRef, useState, + type RefObject, } from "react"; import { FileDiff, @@ -18,7 +19,7 @@ import { MessageSquare, } from "lucide-react"; import { cn } from "@/lib/utils"; -import { layoutRail, type Anchor } from "@/lib/whymark/align"; +import { layoutCards, layoutRail, type Anchor } from "@/lib/whymark/align"; import { NO_DECISIONS, partsOf, @@ -88,6 +89,8 @@ interface FilePanelProps { activeNoteId: string | null; onActivate: (noteId: string | null) => void; compact: boolean; + syncScroll: boolean; + compactRail: boolean; index: number; /** Decisions collected so far for this file. */ decisions?: FileDecisions; @@ -104,6 +107,8 @@ export function FilePanel({ activeNoteId, onActivate, compact, + syncScroll, + compactRail, index, decisions = NO_DECISIONS, onDecisions, @@ -119,16 +124,22 @@ export function FilePanel({ // view would spend half the width on an empty column. const oneSided = file.status === "added" || file.status === "deleted"; const effectiveMode: ViewMode = - mode === "split" && oneSided ? "unified" : mode; + mode === "split" && (oneSided || compact) ? "unified" : mode; const rows = useMemo( () => effectiveMode === "split" - ? toSplitRows(file.rows) + ? toSplitRows(file.rows, !compactRail) : toUnifiedRows(file.rows), - [file.rows, effectiveMode], + [file.rows, effectiveMode, compactRail], ); + const [selectedIds, setSelectedIds] = useState([]); + const chooseNotes = (ids: string[]) => { + const visible = ids.filter(id => visibleNoteIds.has(id)); + setSelectedIds(visible); + if (visible[0]) onActivate(visible[0]); + }; const anchors = useMemo(() => anchorsForRows(rows, notes), [rows, notes]); const [heights, setHeights] = useState>({}); const cardRefs = useRef(new Map()); @@ -158,23 +169,17 @@ export function FilePanel({ return () => observer.disconnect(); }, [measure, notes, compact]); - const rail = useMemo( - () => - layoutRail( - anchors, - (id) => heights[id] ?? 132, - rows.length, - LINE_H, - CARD_GAP, - ), - [anchors, heights, rows.length], - ); + const rail = useMemo(() => { + if (!compactRail) return layoutRail(anchors, id => heights[id] ?? 40, rows.length, LINE_H, CARD_GAP); + const tops = layoutCards(anchors, id => heights[id] ?? 40, row => row * LINE_H, 4); + return { + tops, padding: new Map(), offsets: rows.map(() => 0), + height: Math.max(rows.length * LINE_H, ...anchors.map(anchor => (tops.get(anchor.noteId) ?? 0) + (heights[anchor.noteId] ?? 40))), + }; + }, [anchors, heights, rows, compactRail]); const { tops, padding, offsets } = rail; const railHeight = rail.height; const highlighted = hovered ?? activeNoteId; - const highlightKind = highlighted - ? (file.notes.find((n) => n.id === highlighted)?.kind ?? null) - : null; /* ---- decisions -------------------------------------------------- */ @@ -245,9 +250,9 @@ export function FilePanel({ const rowProps = { notes, highlighted, - highlightKind, onHover: setHovered, onActivate, + onChooseNotes: chooseNotes, decisions, canDecide, onToggle, @@ -344,6 +349,17 @@ export function FilePanel({ + {selectedIds.filter(id => visibleNoteIds.has(id)).length > 1 ? ( + + ) : null}
{notes.filter( (note) => @@ -378,7 +394,7 @@ export function FilePanel({ }} > {effectiveMode === "split" ? ( - + ) : ( )} @@ -400,7 +416,7 @@ export function FilePanel({ return (
void; onActivate: (id: string | null) => void; + onChooseNotes: (ids: string[]) => void; decisions: FileDecisions; canDecide: boolean; onToggle: (row: RowVM) => void; @@ -486,33 +502,69 @@ function UnifiedBody({ function SplitBody({ rows, padding, + syncScroll, ...rowProps -}: RowProps & { rows: DisplayRow[]; padding: Map }) { +}: RowProps & { rows: DisplayRow[]; padding: Map; syncScroll: boolean }) { + const left = useRef(null); + const right = useRef(null); + const last = useRef<"del" | "add">("del"); + useEffect(() => { + const a = left.current; + const b = right.current; + if (!a || !b) return; + const expected = new WeakMap(); + const align = (source: HTMLElement, target: HTMLElement) => { + const next = Math.min(source.scrollLeft, Math.max(0, target.scrollWidth - target.clientWidth)); + if (Math.abs(target.scrollLeft - next) < 0.5) return; + expected.set(target, next); + target.scrollLeft = next; + }; + const scroll = (source: HTMLElement, target: HTMLElement, side: "del" | "add") => { + const pending = expected.get(source); + expected.delete(source); + if (pending !== undefined && Math.abs(source.scrollLeft - pending) < 0.5) return; + last.current = side; + if (syncScroll) align(source, target); + }; + const onLeft = () => scroll(a, b, "del"); + const onRight = () => scroll(b, a, "add"); + const resize = () => { + if (syncScroll) align(last.current === "del" ? a : b, last.current === "del" ? b : a); + }; + a.addEventListener("scroll", onLeft, { passive: true }); + b.addEventListener("scroll", onRight, { passive: true }); + const observer = new ResizeObserver(resize); + observer.observe(a); observer.observe(b); + resize(); + return () => { a.removeEventListener("scroll", onLeft); b.removeEventListener("scroll", onRight); observer.disconnect(); }; + }, [syncScroll]); return (
- + - +
); } function SplitColumn({ + paneRef, side, rows, padding, ...rowProps }: RowProps & { + paneRef: RefObject; side: "add" | "del"; rows: DisplayRow[]; padding: Map; }) { return ( -
+
{rows.map((row, index) => ( @@ -566,18 +618,6 @@ function HunkRow({ ); } -function rowTint( - isHighlighted: boolean, - highlightKind: string | null, - fallback?: string, -): string | undefined { - if (isHighlighted && highlightKind) { - return `color-mix(in oklch, ${ - KIND_META[highlightKind as keyof typeof KIND_META].color - } 13%, transparent)`; - } - return fallback; -} function verdictStyle(verdict: LineVerdict): { className?: string; @@ -600,9 +640,8 @@ function UnifiedRow({ row, notes, highlighted, - highlightKind, onHover, - onActivate, + onChooseNotes, decisions, canDecide, onToggle, @@ -629,11 +668,12 @@ function UnifiedRow({ } const line = row.row!; - const covered = row.noteIds.length > 0; + const noteIds = notes.filter(note => row.noteIds.includes(note.id)).map(note => note.id); + const covered = noteIds.length > 0; const isHighlighted = - highlighted !== null && row.noteIds.includes(highlighted); + highlighted !== null && noteIds.includes(highlighted); const accentKind = covered - ? (notes.find((n) => n.id === row.noteIds[0])?.kind ?? "note") + ? (notes.find((n) => n.id === (isHighlighted ? highlighted : noteIds[0]))?.kind ?? "note") : null; const accent = accentKind ? KIND_META[accentKind].color : null; const verdict = verdictFor(line, decisions); @@ -649,12 +689,15 @@ function UnifiedRow({ return (
row.noteIds.length && onHover(row.noteIds[0])} + data-notes={noteIds.join(" ") || undefined} + data-highlighted={isHighlighted || undefined} + onMouseEnter={() => noteIds.length && onHover(noteIds.includes(highlighted ?? "") ? highlighted : noteIds[0])} onMouseLeave={() => onHover(null)} - onClick={() => row.noteIds.length && onActivate(row.noteIds[0])} + onClick={() => noteIds.length && onChooseNotes(noteIds)} style={{ - backgroundColor: rowTint(isHighlighted, highlightKind, marker.bg), + backgroundColor: marker.bg, + outline: isHighlighted ? "1px solid var(--foreground)" : undefined, + outlineOffset: -1, boxShadow: decorate.boxShadow, }} > @@ -670,6 +713,7 @@ function UnifiedRow({ {line.newLine ?? ""} + 0; + const noteIds = notes.filter(note => cell?.noteIds.includes(note.id)).map(note => note.id); + const covered = noteIds.length > 0; const isHighlighted = - highlighted !== null && row.noteIds.includes(highlighted); + highlighted !== null && noteIds.includes(highlighted); const accentKind = covered - ? (notes.find((n) => n.id === row.noteIds[0])?.kind ?? "note") + ? (notes.find((n) => n.id === (isHighlighted ? highlighted : noteIds[0]))?.kind ?? "note") : null; const accent = accentKind ? KIND_META[accentKind].color : null; @@ -746,8 +790,9 @@ function SplitRow({ return (
@@ -762,20 +807,15 @@ function SplitRow({ return (
row.noteIds.length && onHover(row.noteIds[0])} + data-notes={noteIds.join(" ") || undefined} + data-highlighted={isHighlighted || undefined} + onMouseEnter={() => noteIds.length && onHover(noteIds.includes(highlighted ?? "") ? highlighted : noteIds[0])} onMouseLeave={() => onHover(null)} - onClick={() => row.noteIds.length && onActivate(row.noteIds[0])} + onClick={() => noteIds.length && onChooseNotes(noteIds)} style={{ - backgroundColor: rowTint( - isHighlighted, - highlightKind, - changed - ? side === "add" - ? "var(--whymark-add-bg)" - : "var(--whymark-del-bg)" - : undefined, - ), + backgroundColor: changed ? (side === "add" ? "var(--whymark-add-bg)" : "var(--whymark-del-bg)") : undefined, + outline: isHighlighted ? "1px solid var(--foreground)" : undefined, + outlineOffset: -1, boxShadow: decorate.boxShadow, }} > @@ -788,6 +828,7 @@ function SplitRow({ {side === "add" ? (cell.newLine ?? "") : (cell.oldLine ?? "")} + !right.noteIds.includes(id)))) { + out.push({ key: `p${pair++}`, kind: "pair", noteIds: left.noteIds, left, right: null }); + out.push({ key: `p${pair++}`, kind: "pair", noteIds: right.noteIds, left: null, right }); continue; } out.push({ @@ -1084,11 +1110,6 @@ function toSplitRows(rows: RowVM[]): DisplayRow[] { return out; } -function sameNotes(a: string[], b: string[]): boolean { - if (!a.length || !b.length) return true; - return a.length === b.length && a.every((id) => b.includes(id)); -} - function anchorsForRows(rows: DisplayRow[], notes: Note[]): Anchor[] { const first = new Map(); const last = new Map(); @note +11 kind=intent id=improvement-60 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +22 kind=intent id=improvement-61 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +92..93 kind=intent id=improvement-62 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +110..111 kind=intent id=improvement-63 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +127..142 kind=intent id=improvement-64 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +172..179 kind=intent id=improvement-65 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +255 kind=intent id=improvement-66 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +352..362 kind=intent id=improvement-67 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +397 kind=intent id=improvement-68 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +419 kind=intent id=improvement-69 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +463 kind=intent id=improvement-70 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +505..567 kind=intent id=improvement-71 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note -569..580 kind=intent id=improvement-72 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +644 kind=intent id=improvement-73 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +671..676 kind=intent id=improvement-74 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +692..700 kind=intent id=improvement-75 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +716 kind=intent id=improvement-76 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +753 kind=intent id=improvement-77 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +780..785 kind=intent id=improvement-78 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +793..795 kind=intent id=improvement-79 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +810..818 kind=intent id=improvement-80 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +831 kind=intent id=improvement-81 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +1058 kind=intent id=improvement-82 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note +1095..1097 kind=intent id=improvement-83 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @note -1087..1091 kind=intent id=improvement-84 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @file src/components/whymark/note-card.tsx modified +16 -9 oldsha=2a7182f newsha=b11e8c5 @@ -1,9 +1,10 @@ "use client"; import { forwardRef } from "react"; -import { CircleHelp, ListTodo, Shuffle, Waypoints } from "lucide-react"; +import { ChevronDown, ChevronRight, CircleHelp, ListTodo, Shuffle, Waypoints } from "lucide-react"; import { cn } from "@/lib/utils"; import type { Note } from "@/lib/whymark/types"; +import { useNoteDisclosure } from "./note-disclosure"; import { KIND_META } from "./meta"; import { Markdown, inline } from "./markdown"; import { Confidence, RiskPill, SourceRow, VerifyRow } from "./pills"; @@ -22,5 +23,6 @@ export const NoteCard = forwardRef(function NoteC { note, active, dimmed, onHover, onSelect, scopeLabel }, ref, ) { + const { expanded, toggle } = useNoteDisclosure(note.id); const kind = KIND_META[note.kind] ?? KIND_META.note; const Icon = kind.icon; @@ -31,13 +33,14 @@ export const NoteCard = forwardRef(function NoteC id={`note-${note.id}`} onMouseEnter={() => onHover?.(note.id)} onMouseLeave={() => onHover?.(null)} - onClick={() => onSelect?.(note.id)} + onFocus={() => onHover?.(note.id)} + onBlur={() => onHover?.(null)} className={cn( "group cursor-default rounded-lg border bg-card/80 shadow-sm backdrop-blur-[2px] transition-all", active ? "border-transparent ring-1 shadow-md" : "border-border/60 hover:border-border", - dimmed && "opacity-35", + dimmed && "opacity-65", )} style={{ borderLeft: `2px solid ${kind.color}`, @@ -49,7 +52,10 @@ export const NoteCard = forwardRef(function NoteC : {}), }} > -
+
+ (function NoteC {kind.label} - + - {note.risk ? : null} - {note.confidence !== undefined ? : null} + {note.urgency ?? "unspecified"} + {note.confidence !== undefined ? : confidence unknown}
-
+ @note +65 kind=intent id=improvement-90 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +74 kind=intent id=improvement-91 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +131 kind=intent id=improvement-92 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @file src/components/whymark/review-view.tsx modified +52 -15 oldsha=931b78a newsha=bab3424 @@ -27,6 +27,12 @@ import { CoverageBar, Ring, Stat } from "./meters"; import { Markdown } from "./markdown"; import { NoteCard } from "./note-card"; import { useMediaQuery } from "./use-media-query"; +import { QualityPanel } from "./quality-panel"; +import { EvidencePanel } from "./evidence-panel"; +import { NoteDisclosure } from "./note-disclosure"; +import { ReviewSettings } from "./review-settings"; +import { useReviewPreferences } from "./use-review-preferences"; +import { type ReviewPreferences } from "@/lib/review-preferences"; import { hasPassingVerify } from "@/lib/whymark/stats"; interface ReviewViewProps { @@ -40,8 +46,28 @@ interface ReviewViewProps { export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { const compact = !useMediaQuery("(min-width: 1180px)", true); - const [mode, setMode] = useState("unified"); + const [preferences, setPreferences] = useReviewPreferences(); const [activeNoteId, setActiveNoteId] = useState(null); + const [disclosures, setDisclosures] = useState>({}); + const updatePreferences = (next: ReviewPreferences) => { + setPreferences(next); + if (next.collapseNotes !== preferences.collapseNotes) setDisclosures({}); + }; + const activate = useCallback((id: string | null) => { + setActiveNoteId(id); + if (id) setDisclosures(current => ({ ...current, [id]: true })); + }, []); + useEffect(() => { + const reveal = () => { + let hash: string; + try { hash = decodeURIComponent(window.location.hash.slice(1)); } catch { return; } + if (hash.startsWith("note-")) activate(hash.slice(5)); + }; + const frame = requestAnimationFrame(reveal); + window.addEventListener("hashchange", reveal); + return () => { cancelAnimationFrame(frame); window.removeEventListener("hashchange", reveal); }; + }, [activate]); + const [mode, setMode] = useState("unified"); const [kindFilter, setKindFilter] = useState>(new Set()); const [onlyUnverified, setOnlyUnverified] = useState(false); const [onlyInference, setOnlyInference] = useState(false); @@ -74,10 +100,8 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { const orderedVisible = useMemo( () => - review.files - .flatMap((file) => file.notes) - .filter((note) => visibleNoteIds.has(note.id)), - [review.files, visibleNoteIds], + allNotes.filter((note) => visibleNoteIds.has(note.id)), + [allNotes, visibleNoteIds], ); const jump = useCallback( @@ -89,13 +113,13 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { (index + delta + orderedVisible.length * 2) % orderedVisible.length ]; if (!next) return; - setActiveNoteId(next.id); + activate(next.id); const element = document.getElementById(`note-${next.id}`); element?.scrollIntoView({ behavior: "smooth", block: "center" }); element?.classList.remove("whymark-flash"); requestAnimationFrame(() => element?.classList.add("whymark-flash")); }, - [orderedVisible, activeNoteId], + [orderedVisible, activeNoteId, activate], ); const setFileDecisions = useCallback((path: string, next: FileDecisions) => { @@ -215,7 +239,7 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { const onKey = (event: KeyboardEvent) => { if (event.metaKey || event.ctrlKey || event.altKey) return; const target = event.target as HTMLElement | null; - if (target && /input|textarea|select/i.test(target.tagName)) return; + if (target && (/input|textarea|select/i.test(target.tagName) || target.isContentEditable || target.closest('[role="dialog"]'))) return; switch (event.key) { case "j": @@ -255,10 +279,14 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { (issue) => issue.code === "review-stale" || issue.code === "file-missing", ); const problems = issues.filter( - (issue) => issue.level === "error" || issue.code === "note-stub", + (issue) => issue.level === "error" || ["note-stub", "source-unavailable", "evidence-invalid", "quality-invalid"].includes(issue.code), ); return ( + disclosures[id] ?? !preferences.collapseNotes, + toggle: id => setDisclosures(current => ({ ...current, [id]: !(current[id] ?? !preferences.collapseNotes) })), + }}>
{onBack ? ( @@ -295,6 +323,6 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { /> - {Math.round(stats.verifiedCoverage * 100)}% verified + {Math.round(stats.verifiedCoverage * 100)}% claimed verified
@@ -335,6 +363,8 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) {
+ +
@@ -520,6 +550,8 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) {
+
+ {/* filters */}
@@ -589,6 +621,8 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { ) : null} + + {visibleNoteIds.size} of {allNotes.length} shown ·{" "} j{" "} @@ -606,9 +640,11 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { index={index} mode={mode} compact={compact} + syncScroll={preferences.syncScroll} + compactRail={preferences.compactRail} visibleNoteIds={visibleNoteIds} activeNoteId={activeNoteId} - onActivate={setActiveNoteId} + onActivate={activate} editable={Boolean(slug)} decisions={decisions[file.path]} onDecisions={setFileDecisions} @@ -643,5 +679,6 @@ export function ReviewView({ review, slug, issues, onBack }: ReviewViewProps) { {showHelp ? setShowHelp(false)} /> : null}
+ ); } @note +30..35 kind=intent id=improvement-93 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +49..70 kind=intent id=improvement-94 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +103..104 kind=intent id=improvement-95 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +116..122 kind=intent id=improvement-96 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +242 kind=intent id=improvement-97 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +282..289 kind=intent id=improvement-98 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +326 kind=intent id=improvement-99 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +366..367 kind=intent id=improvement-100 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +437..445 kind=intent id=improvement-101 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +468 kind=intent id=improvement-102 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +553..554 kind=intent id=improvement-103 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +624..625 kind=intent id=improvement-104 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +643..647 kind=intent id=improvement-105 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +682 kind=intent id=improvement-106 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @file src/lib/view-model.ts modified +11 -1 oldsha=18a4286 newsha=e0beaba @@ -1,3 +1,6 @@ +import { readQuality } from "./quality/import"; +import type { QualityReport } from "./quality/types"; +import { evidenceViews, type EvidenceView } from "./whymark/evidence"; import { buildRows } from "@/lib/whymark/align"; import { inlineParts, linePairs, type InlinePart } from "@/lib/whymark/inline"; import { languageFor } from "@/lib/whymark/lang"; @@ -42,6 +45,9 @@ export interface FileVM { } export interface ReviewVM { + evidence: EvidenceView[]; + quality: QualityReport | null; + qualityState: "current" | "stale" | "unchecked"; meta: Meta; files: FileVM[]; docNotes: Note[]; @@ -51,7 +57,7 @@ export interface ReviewVM { export async function buildReviewVM( doc: WhymarkDocument, - options: { isWritable?: (file: FileSection) => boolean } = {}, + options: { isWritable?: (file: FileSection) => boolean; evidence?: EvidenceView[]; source?: string | null; reviewHash?: string } = {}, ): Promise { const stats = computeStats(doc); const files: FileVM[] = []; @@ -108,7 +114,11 @@ export async function buildReviewVM( }); } + const quality = readQuality(doc.meta.extra.quality); return { + quality, + qualityState: !quality || !options.source ? "unchecked" : quality.source === options.source && quality.sourceAfter === options.source && quality.reviewHash === options.reviewHash ? "current" : "stale", + evidence: options.evidence ?? evidenceViews(doc), meta: doc.meta, files, docNotes: doc.notes, @note +1..3 kind=intent id=improvement-107 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +48..50 kind=intent id=improvement-108 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +60 kind=intent id=improvement-109 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @note +117..121 kind=intent id=improvement-110 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @file src/lib/whymark/parse.ts modified +19 -1 oldsha=03555b2 newsha=b858bcc @@ -1,6 +1,8 @@ import YAML from "yaml"; import { WHYMARK_VERSION, + URGENCIES, + type Urgency, NOTE_KINDS, VERIFY_METHODS, type WhymarkDocument, @@ -64,6 +66,7 @@ const KNOWN_FIELDS = new Set([ ...REPEATABLE, "kind", "risk", + "urgency", "confidence", "id", ]); @@ -760,6 +763,11 @@ function parseNoteHeader( }); } + const urgency = URGENCIES.includes(attrs.urgency as Urgency) ? attrs.urgency as Urgency : undefined; + if (attrs.urgency && !urgency) diagnostics.push({ + level: "warning", code: "urgency-unknown", message: `Unknown urgency ${attrs.urgency}; preserved as a field.`, line: lineNo, + }); + let confidence: number | undefined; if (attrs.confidence) { const value = Number(attrs.confidence.replace("%", "")); @@ -796,6 +804,7 @@ function parseNoteHeader( selector, kind, risk, + urgency, confidence, sources: [], verify: [], @@ -804,7 +813,7 @@ function parseNoteHeader( questions: [], refs: [], body: "", - extra: {}, + extra: attrs.urgency && !urgency ? { urgency: [attrs.urgency] } : {}, sourceLine: lineNo, file: filePath, }; @@ -899,6 +908,15 @@ function setField( } } + if (key === "urgency") { + if (URGENCIES.includes(value as Urgency)) { + note.urgency = value as Urgency; + ctx.lastField = null; + return; + } + diagnostics.push({ level: "warning", code: "urgency-unknown", message: `Unknown urgency ${value}; preserved as a field.`, line: lineNo, noteId: note.id }); + } + // Attributes are allowed as fields too, which is how models often write them. if (key === "kind" || key === "risk" || key === "confidence" || key === "id") { const num = Number(value.replace("%", "")); @note +4..5 kind=intent id=improvement-111 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +69 kind=intent id=improvement-112 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +766..770 kind=intent id=improvement-113 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +807 kind=intent id=improvement-114 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +816 kind=intent id=improvement-115 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +911..919 kind=intent id=improvement-116 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/serialize.ts modified +1 oldsha=f44d985 newsha=d2c0be6 @@ -135,6 +135,7 @@ export function serializeNote(note: Note, wrap = 78): string { const field = (name: string, value: string) => lines.push(...wrapField(name, value, wrap)); + if (note.urgency) field("urgency", note.urgency); if (note.why) field("why", note.why); if (note.what) field("what", note.what); for (const source of note.sources) field("source", source.raw); @note +138 kind=intent id=improvement-117 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/stats.ts modified +1 -1 oldsha=652966e newsha=c60f893 @@ -70,7 +70,7 @@ export function coveredLines(file: FileSection): { } export function hasPassingVerify(note: Note): boolean { - return note.verify.some((v) => v.status === "pass" && v.method !== "none"); + return !note.verify.some(v => v.status === "fail") && note.verify.some((v) => v.status === "pass" && v.method !== "none"); } export function addedLineNumbers(file: FileSection): number[] { @note +73 kind=intent id=improvement-118 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/types.ts modified +4 oldsha=092b6f7 newsha=412be80 @@ -50,6 +50,9 @@ export const NOTE_KINDS: NoteKind[] = [ "note", ]; +export const URGENCIES = ["info", "normal", "urgent", "blocking"] as const; +export type Urgency = (typeof URGENCIES)[number]; + export type Risk = "low" | "medium" | "high"; export type VerifyStatus = "pass" | "fail" | "unknown" | "skipped"; @@ -121,6 +124,7 @@ export interface Note { selector: Selector; kind: NoteKind; risk?: Risk; + urgency?: Urgency; confidence?: number; why?: string; what?: string; @note +53..55 kind=intent id=improvement-119 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +127 kind=intent id=improvement-120 urgency: normal why: Urgency means action priority, not risk or measured correctness. Emit it as a body field because the baseline v1 reader preserves unknown fields but discards unknown header attributes; retain invalid/unknown evidence without interpreting it as success. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/validate-tree.ts modified +43 -3 oldsha=39b9f6a newsha=50b69c4 @@ -1,12 +1,13 @@ -import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { join, relative, resolve, sep } from "node:path"; import { hashObject } from "./git"; import { validateDocument, type ValidateOptions, type ValidationResult, } from "./validate"; -import type { Diagnostic, WhymarkDocument } from "./types"; +import { allNotes, type Diagnostic, type WhymarkDocument } from "./types"; /** Working-tree hash comparison — Node only, not used by the hosted viewer. */ export function collectStaleness(doc: WhymarkDocument, cwd: string): Diagnostic[] { @@ -52,5 +53,44 @@ export function validateDocumentInRepo( const extra = options.skipStaleness ? [] : collectStaleness(doc, options.cwd ?? process.cwd()); + if (!options.skipStaleness) extra.push(...collectSourceDiagnostics(doc, options.cwd ?? process.cwd())); return validateDocument(doc, { ...options, extraDiagnostics: extra }); } + +export function collectSourceDiagnostics(doc: WhymarkDocument, cwd: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + for (const note of allNotes(doc)) for (const source of note.sources) { + let problem: string | null = null; + try { + if (source.type === "file") { + const match = source.locator.match(/^(.*?)(?::(\d+)(?:-(\d+))?)?$/); + if (!match || /[*?]/.test(match[1])) continue; + const path = match[1]; + if (path.startsWith("/") || path.split(/[\\/]/).includes("..")) throw new Error("reference escapes repository"); + let content: string; + const revision = doc.meta.scope === "commit" ? doc.meta.head?.match(/[a-f0-9]{7,40}$/)?.[0] : undefined; + if (doc.meta.scope === "commit" && !revision) throw new Error("recorded commit unavailable"); + if (revision) content = execFileSync("git", ["show", `${revision}:${path}`], { cwd, encoding: "utf8", maxBuffer: 2 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }); + else { + const root = realpathSync(cwd); + const absolute = realpathSync(resolve(root, path)); + const rel = relative(root, absolute); + if (rel === ".." || rel.startsWith(`..${sep}`)) throw new Error("reference escapes repository"); + content = readFileSync(absolute, "utf8"); + } + const count = content.replace(/\n$/, "").split("\n").length; + if (match[2] && (Number(match[2]) < 1 || Number(match[3] ?? match[2]) > count || Number(match[3] ?? match[2]) < Number(match[2]))) problem = "line range is outside the file"; + } else if (source.type === "commit") { + if (!/^[a-f0-9]{7,40}$/.test(source.locator)) throw new Error("expected a commit hash"); + execFileSync("git", ["cat-file", "-e", `${source.locator}^{commit}`], { cwd, stdio: "ignore" }); + } else if (source.type === "dep") { + const match = source.locator.match(/^(@?[^@]+)@(.+)$/); + if (!match || !/^(@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/i.test(match[1])) throw new Error("expected dependency@version"); + const pkg = JSON.parse(readFileSync(join(cwd, "node_modules", match[1], "package.json"), "utf8")); + if (pkg.version !== match[2]) problem = "installed dependency version differs from the citation"; + } + } catch { problem = "reference unavailable at the reviewed location"; } + if (problem) diagnostics.push({ level: "warning", code: "source-unavailable", message: `${source.locator}: ${problem}. Locator checks do not establish that a source supports the claim.`, noteId: note.id, file: note.file ?? undefined }); + } + return diagnostics; +} @note +1..10 kind=intent id=improvement-121 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +56..96 kind=intent id=improvement-122 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/validate.ts modified +9 oldsha=c21c915 newsha=ee47fba @@ -1,2 +1,4 @@ +import { readEvidence } from "./evidence"; +import { readQuality } from "../quality/import"; import { computeStats, hasPassingVerify, type DocStats } from "./stats"; import { allNotes, isStub, type WhymarkDocument, type Diagnostic } from "./types"; @@ -44,6 +46,13 @@ export function validateDocument( }); } + if (doc.meta.extra.evidence !== undefined && (!Array.isArray(doc.meta.extra.evidence) || readEvidence(doc.meta.extra.evidence).length !== doc.meta.extra.evidence.length)) { + diagnostics.push({ level: "warning", code: "evidence-invalid", message: "Malformed execution evidence was preserved but cannot establish verification." }); + } + if (doc.meta.extra.quality !== undefined && !readQuality(doc.meta.extra.quality)) { + diagnostics.push({ level: "warning", code: "quality-invalid", message: "Malformed quality report was preserved but cannot establish a clean scan." }); + } + if (!doc.meta.summary?.trim()) { diagnostics.push({ level: "warning", @note +1..2 kind=intent id=improvement-123 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +49..55 kind=intent id=improvement-124 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/verify.ts modified +27 -2 oldsha=79aa649 newsha=cc4337b @@ -1,2 +1,4 @@ +import { readEvidence, type ExecutionEvidence } from "./evidence"; +import { gitHead, reviewFingerprint, sourceFingerprint, writeOutput } from "./evidence-node"; import { spawnSync } from "node:child_process"; import { allNotes, type Check, type WhymarkDocument, type Note, type VerifyClaim } from "./types"; @@ -10,6 +12,7 @@ export interface RunResult { durationMs: number; output: string; timedOut: boolean; + unavailable: boolean; } export interface ClaimResult { @@ -25,6 +28,8 @@ export interface ClaimResult { export interface VerifyOptions { cwd?: string; + recordEvidence?: boolean; + toolVersion?: string; timeoutMs?: number; /** Only run commands matching this pattern. */ filter?: RegExp; @@ -49,6 +54,7 @@ export function runCommand(cmd: string, options: VerifyOptions = {}): RunResult status: result.status === 0 ? "pass" : "fail", durationMs: Date.now() - started, output, + unavailable: Boolean(result.error || result.status === null || result.status === 127), timedOut: Boolean(result.error && /ETIMEDOUT|timed out/i.test(String(result.error))), }; } @@ -59,6 +65,9 @@ export function verifyDocument( options: VerifyOptions = {}, ): ClaimResult[] { const results: ClaimResult[] = []; + const cwd = options.cwd ?? process.cwd(); + const before = options.recordEvidence ? sourceFingerprint(cwd) : null; + const recordedHead = options.recordEvidence ? gitHead(cwd) : null; const cache = new Map(); const run = (cmd: string): RunResult => { @@ -78,7 +87,7 @@ export function verifyDocument( file: null, claimed: check.status, cmd: check.cmd, - outcome: outcomeFor(check.status, result.status), + outcome: result.unavailable ? "unrunnable" : outcomeFor(check.status, result.status), run: result, }; applyToCheck(check, result); @@ -112,7 +121,7 @@ export function verifyDocument( file: note.file, claimed: claim.status, cmd, - outcome: outcomeFor(claim.status, result.status), + outcome: result.unavailable ? "unrunnable" : outcomeFor(claim.status, result.status), run: result, }; applyToClaim(claim, result); @@ -121,5 +130,21 @@ export function verifyDocument( } } + if (options.recordEvidence) { + const after = sourceFingerprint(cwd); + const previous = readEvidence(doc.meta.extra.evidence); + const additions: ExecutionEvidence[] = results.filter(result => result.run).map(result => { + const run = result.run!; + return { + version: 1, command: result.cmd, noteId: result.noteId, file: result.file, + claimed: result.claimed, status: run.unavailable ? "unavailable" : run.status, + ran: new Date().toISOString(), durationMs: run.durationMs, exitCode: run.exitCode, + cwd: ".", tool: `whymark@${options.toolVersion ?? "development"}`, runtime: process.version, + source: before, sourceAfter: after, head: recordedHead, base: doc.meta.base ?? null, + reviewHash: reviewFingerprint(doc), ...writeOutput(cwd, run.output), summary: summarise(run), + }; + }); + doc.meta.extra.evidence = [...previous.filter(old => !additions.some(next => next.command === old.command && next.noteId === old.noteId && next.file === old.file)), ...additions]; + } return results; } @note +1..2 kind=intent id=improvement-125 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +15 kind=intent id=improvement-126 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +31..32 kind=intent id=improvement-127 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +57 kind=intent id=improvement-128 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +68..70 kind=intent id=improvement-129 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +90 kind=intent id=improvement-130 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +124 kind=intent id=improvement-131 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @note +133..148 kind=intent id=improvement-132 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/e2e/viewer.mjs modified +2 oldsha=6de9ecb newsha=8617553 @@ -17,6 +17,8 @@ const check = (name, ok, detail = "") => const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); +await page.addInitScript(() => localStorage.setItem("whymark.preferences.v1", JSON.stringify({ version: 1, syncScroll: true, collapseNotes: false, compactRail: false }))); + const consoleErrors = []; page.on("console", (msg) => { if (msg.type() === "error") consoleErrors.push(msg.text()); @note +20..21 kind=intent id=improvement-133 urgency: normal why: Exercise observable behavior and failure boundaries rather than only matching implementation text: overlapping selection, reversible file decisions, evidence freshness, unsafe destinations and scanner cancellation must remain reliable. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run test:ui` => pass (exit 0 in 45.1s) @file specs/001-review-improvements/spec.md added +290 newsha=ce164b7 @@ -0,0 +1,290 @@ +# Review improvements: requirements and delivery roadmap + +Status: initial delivery implemented; see `verification.md`. Date: 2026-09-17. +Baseline inspected: `28e5280`; working trees were clean before this documentation work. +Classification: Large — viewer interaction, a format extension, npm distribution, +and evidence ingestion need separate, independently verifiable deliveries. + +## Goal + +Make AI-authored changes easier to inspect without losing the distinction between +what an author claims and what tools actually checked. Support several precise +annotations on the same code, compact disclosure, stable diff colors, synchronized +split scrolling, an npm-delivered agent skill, and deterministic quality checks. + +## Scope and assumptions + +- “Comments” initially means existing line-addressed `@note` annotations. Multiple + annotations on a range are required; discussion replies and browser authoring are + a separate proposal below, because they need authorship and persistence semantics. +- “Estimated accuracy” means explicitly labeled author confidence. Do not invent a + measured accuracy percentage from coverage, passing tests, citations, or lint. +- Urgency is distinct from risk: priority to act versus blast radius if wrong. +- Retain v1 reading and existing line/part/hunk apply behavior. +- The initial task registered Vault context; the subsequent implementation request + authorized the deliveries below. Publishing, deployment, commits and global skill + installation remain separate actions. +- No new hosted backend, accounts, AI service, or automatic code rewrite in this scope. + +## Observed baseline and files to read + +| Files | Current behavior and implication | +| --- | --- | +| `spec/whymark-v1.md`, `src/lib/whymark/types.ts`, `parse.ts`, `serialize.ts` | Repeated `@note` selectors already work; confidence and risk exist, urgency does not. | +| `src/lib/whymark/align.ts`, `src/lib/view-model.ts` | Rows carry `noteIds[]`; `layoutRail` inserts code padding to align cards. | +| `src/components/whymark/file-panel.tsx` | Row hover/click uses `noteIds[0]`; `rowTint` replaces diff tone with category tone. Split panes scroll independently. | +| `src/components/whymark/note-card.tsx`, `pills.tsx`, `meta.tsx` | Expanded cards, type/risk/confidence badges; no per-card disclosure. | +| `src/components/whymark/review-view.tsx`, `src/components/ui/dialog.tsx` | Existing navigation, filters and review state; reuse installed dialog primitives for settings. | +| `src/components/whymark/code-line.tsx`, `src/lib/whymark/inline.ts`, `edit.ts` | Shared inline parts are the contract between highlighting and file writes. | +| `src/lib/whymark/stats.ts`, `verify.ts`, `validate-tree.ts` | Passing claims count toward verified coverage; CLI reruns shell commands; blob hashes detect stale files. | +| `src/cli/whymark.ts`, `src/cli/help.ts`, `package.json`, `bin/whymark.mjs` | Existing CLI and npm packaging; package allowlist excludes `.agents/skills/whymark`. | +| `.agents/skills/whymark/SKILL.md`, `prompts/whymark-author.md` | Existing guidance already asks for tight ranges, real sources, and honest verification. | +| `tests/parse.test.ts`, `tests/review.test.ts`, `tests/cli.test.ts`, `tests/e2e/viewer.mjs` | Extend existing unit/CLI/browser patterns; preserve edit/apply regressions. | + +The hover cause is supported by source inspection. This planning task has not +reproduced the reported interaction in a browser; capture that baseline before fixing it. + +## Delivery sequence + +| Order | Package | Depends on | Completion boundary | +| --- | --- | --- | --- | +| 1 | WM-01: semantic diff colors | none | Category hover cannot turn an addition into a red deletion-like row. | +| 2 | WM-02: multiple compact comments | WM-01 | Every overlapping note reachable; disclosure removes padding pressure. | +| 3 | WM-03: settings and split scrolling | WM-02 | Bidirectional horizontal sync, accessible settings, independent mode. | +| 4 | WM-04: npm skill distribution | WM-02 contract | Packed artifact installs the skill without Git access. | +| 5 | WM-05: trustworthy evidence presentation | none; integrate after WM-02 | Claims, fresh execution, stale evidence and inference distinguished. | +| 6 | WM-06: deterministic quality findings | WM-05 | Versioned findings tied to the reviewed source, with baseline comparison. | + +Each package should be a small reviewable change with its own passing checks. +WM-04 can proceed independently once the annotation contract and guidance settle. +Do not wait for the scanner to ship the UI fixes. + +## WM-01 — preserve diff meaning during hover + +**Change:** modify `file-panel.tsx`, `code-line.tsx` only if necessary, and +`src/app/globals.css`; extend `tests/e2e/viewer.mjs`. + +- Addition/deletion backgrounds remain green/red during hover, selection and focus. +- Indicate the annotation using a gutter marker or outline; keep category color on + its badge/card. Do not replace diff backgrounds or syntax-token colors. +- Preserve distinct restored, rejected and partially reverted states. +- Test a security note on an addition, a deletion and unchanged context in unified + and split views, including overlapping annotations and keyboard focus. +- Capture before/after screenshots and computed style assertions. The existing + part-revert/apply tests must still show exactly the bytes that will be written. + +## WM-02 — multiple comments, disclosure and tighter layout + +**Change:** `note-card.tsx`, `file-panel.tsx`, `review-view.tsx`, `pills.tsx`, +`align.ts`, relevant types/parser/serializer/validator, format documentation, +author prompt, skill, `tests/parse.test.ts`, `tests/review.test.ts`, and browser tests. +Add focused `tests/align.test.ts` if existing tests cannot cover layout cleanly. + +- Keep all matching note IDs, with stable document order. A line shows a count; + selecting it exposes all applicable notes, with keyboard-selectable entries. + Filtering must not leave a hidden first note intercepting selection. +- Each card has a real disclosure button with `aria-expanded` and `aria-controls`. + Collapsed cards retain type, urgency and author confidence; missing values show + “unspecified”/“unknown”. Keep the line selector visible for orientation. +- Proposed new optional header: `urgency=info|normal|urgent|blocking`. Never infer + blocking from `kind=security`, and never reinterpret existing `risk` as urgency. +- Add urgency across spec, types, parser, serializer, validation, examples, skill and + author prompt together. Preserve legacy input and unknown values without data loss; + fixture-test old readers' behavior before choosing an additive v1 release. If they + cannot preserve it, decide versioning before shipping the extension. +- Keep disclosure state separate from code acceptance/rejection and note selection. + Clicking a link or disclosure must not reject code. Keyboard navigation to a + collapsed note reveals it. Provide collapse-all/expand-all with per-note override. +- Proposed default: compact collapsed notes; selecting a note expands that note. +- Group notes sharing an anchor. Use measured collapsed/expanded heights and a + compact rail that does not insert blank code rows to accommodate long comments. + Connect displaced cards to their exact range; allow explicit selection to bring + code and card into view. Keep both code columns vertically aligned. +- At 1600px, a fixture with three long notes on adjacent lines must retain the + ordinary 21px code-line rhythm; expanding a note must not create card overlap. + At 390px, notes remain reachable with no page-wide horizontal overflow. +- Author guidance: one claim/reason per note, narrowest meaningful range, first + sentence states the decision and consequence, evidence explains why the source + supports the claim. Do not fill every line with repetitive commentary to game coverage. +- Tests: three notes on one line, partially overlapping old/new ranges, file/doc notes, + unknown urgency, missing confidence, round-trip preservation, filtering, deep links, + resize, keyboard navigation and disclosure after changing view mode. + +## WM-03 — settings modal and synchronized split scrolling + +**Change:** `review-view.tsx`, `file-panel.tsx`, new +`src/components/whymark/review-settings.tsx`, new `src/lib/review-preferences.ts`, +and browser tests. Reuse `src/components/ui/dialog.tsx`. + +- Settings button opens a labeled modal with focus containment, Escape dismissal, + keyboard switches and focus return to the opener. +- First settings: synchronize horizontal scrolling (default on), default comment + disclosure, and compact versus expanded annotation presentation. Add reset defaults. +- Synchronize only the two sides of the same file in either direction. Equal pixel + offsets are preferable to percentages because code columns should line up. Clamp + the destination to its available range without moving the originating pane back. +- Avoid feedback loops from programmatic scroll events, handle resize/short panes, + and clean up handlers. No coupling between files or forced vertical scrolling. +- Disabling sync lets each pane move independently. Re-enabling aligns to the last + user-scrolled pane. Switching unified/split preserves the preference. +- Persist only harmless viewer preferences in existing browser-local storage style, + with versioning and an in-memory fallback for blocked/corrupt storage. Review text, + evidence and code decisions must not enter the preferences object. +- Test both directions, unequal widths, no overflow, rapid alternating scrolls, + toggling off/on, multiple files, reload, blocked storage and modal accessibility. + +## WM-04 — install the agent skill using npm + +**Change:** `package.json`, `src/cli/whymark.ts`, `src/cli/help.ts`, +`.agents/skills/whymark/SKILL.md`, `prompts/whymark-author.md`, `README.md`, +`tests/cli.test.ts`; add `tests/package.test.ts` and a small installer module if useful. + +- Keep the existing package; explicitly include the canonical skill and required + references in its npm file allowlist. Do not depend on cloning the private repo. +- Proposed command (not implemented): `npx whymark@ skill install`. + Project-local installation is default; explicit agent and global options select + destinations. Start with Codex and Claude Code, then extend documented targets. +- Resolve bundled files relative to the installed package, not the current checkout. + All relative skill links must resolve in the installed skill. Include a minimal + working review example and the relevant format reference with the installation. +- Preview destinations; identical installs are idempotent. Refuse to overwrite + customized files without explicit replacement. Detect unsafe symlink destinations. + Do not modify unrelated `AGENTS.md`/`CLAUDE.md` or run postinstall mutations. +- Keep `init` as its existing alias for `new` (review skeleton generation); document + `skill install` separately so installation cannot replace that command's behavior. +- Packaging gate: build CLI, `npm pack`, inspect tarball, install into a temporary + consumer with no repository credentials, then execute the packaged installer. + Test project/global targets using an isolated test home, repeats and conflicts. +- Public npm content is public even if the repository is private; review the tarball + allowlist before release. Publishing remains a separate authorized action. +- `npx skills` does support private repositories using configured authentication, + but that still requires repository access. npm delivery avoids that requirement. + +## WM-05 — evidence a reviewer can inspect + +**Change:** `src/lib/whymark/stats.ts`, `verify.ts`, `validate-tree.ts`, `types.ts`, +`src/components/whymark/review-view.tsx`, `pills.tsx`, and new evidence model/tests. +Any persisted evidence schema also updates parser, serializer, spec and skill. + +- Rename current “verified” presentation to “claimed verified” unless backed by a + recorded execution bound to this source state. Preserve old reports as legacy claims. +- Show distinct states: author claim, execution record, stale record, contradicted, + unavailable, skipped. A pass plus a failure cannot render as unqualified success. +- Record command, cwd relative to repo, tool version, execution time, exit code, + output artifact/hash, and source fingerprint including relevant uncommitted files. + Record the reviewed base/head and test/config/lockfile inputs as well as diff hashes. +- Hash mismatch invalidates freshness; hashes establish identity, not who ran a check. + Imported results remain externally supplied evidence unless independently rerun or + authenticated. Do not call a successful check proof of correctness. +- Show source locator plus the supported claim. Validate local file/line/commit/dep + references against the recorded revision. A reachable URL is not proof it supports + the explanation; semantic relevance remains a reviewer judgment. +- Sources stay clickable but are not automatically fetched by the server. Optional + link checking must be separate, bounded, and protect private/internal network targets. +- Viewing/importing a review never executes its embedded commands. The existing CLI + verifier runs shell text; show exact commands and require explicit execution intent + before adding any UI runner. Use trusted configured checks for automation. +- Tests: fabricated pass with no run, stale tests with unchanged production file, + pass/fail conflict, missing output, timeout, skipped tool, inference-only source, + missing local reference, and hostile imported command that is never auto-executed. + +## WM-06 — automatic checks without AI + +**Change:** new `src/lib/quality/types.ts`, `src/lib/quality/import.ts`, +`src/lib/quality/baseline.ts`, `src/lib/quality/run.ts`, quality tests, +CLI/help, new `src/components/whymark/quality-panel.tsx`, and optional +`whymark.config.json` for explicitly configured local checks. + +Start with existing scripts and an ESLint JSON adapter, then add adapters as separate +deliveries. Do not add all tools as mandatory runtime dependencies. + +| Signal | Tool / approach | Limitation | +| --- | --- | --- | +| Type and rule violations | Existing TypeScript and ESLint; configurable complexity/depth/size warnings | Cannot establish domain correctness or subjective readability. | +| Unused exports/files/dependencies | Optional Knip adapter | Requires project-aware configuration and allowances for dynamic usage. | +| Security patterns | Optional Semgrep CE with pinned local rules | Rule-dependent; findings require triage, not automatic rejection. | +| Dependency advisories | Optional npm audit import | Registry-backed/networked; not an offline scan or exploitability proof. | +| Test evidence | Existing test runner and coverage report import | Line coverage is not assertion quality or behavioral completeness. | + +- Normalize tool/version, rule ID, severity, message, file/range, help URL, + fingerprint, source revision, status and artifact reference. Validate import size, + paths and malformed reports; never treat tool output as instructions. +- Compare against the base revision to separate new findings from existing debt. + Run tools with full project/dependency context and filter presentation to changed + lines plus related findings; do not assume linting changed files proves the project. +- Stable identities account for shifted lines and renames. Expose unanchored findings + at file/project level rather than dropping them. A crashed/missing tool is unavailable, + never a clean scan. Baseline failure means comparison unavailable, not no new issues. +- Initially advisory. Later opt-in CI policy can block new errors/high-severity findings, + with explicit, reviewable suppressions carrying reason and expiry. +- Automatic mode runs only trusted repository-configured tools after explicit setup, + debounces changes, cancels superseded jobs and prevents stale results replacing newer + ones. Imported `.whymark` commands cannot define that configuration. +- Tests: pre-existing/new/resolved finding, rename, line shift, deleted file, missing + baseline, scanner failure, excessive output, suppression expiry and out-of-order jobs. + +## Further features worth considering + +1. Reviewer checklist: inspect high-risk changes, unresolved assumptions, missing tests + and stale evidence before marking a review complete; no universal trust score. +2. Reviewed/unreviewed state tied to file hashes, so changed code reopens review work. +3. Regression and mutation-test evidence for critical paths, with cost controls; + optional later, because mutation testing can be slow and environment-sensitive. +4. Change impact from imports/callers and linked tests, explicitly labeled as a static + approximation. Useful for spotting a correct local change with missed consumers. +5. Human replies and resolution history, if needed: stable thread IDs, author/time, + export/persistence rules, and reopen-on-code-change behavior before adding the UI. +6. Structured JSON/SARIF import/export once the first native adapter is stable; avoid + making every finding an AI-authored annotation or inflating explanation coverage. + +## Verification and rollout + +- Every package: `npm test`, `npm run typecheck`, `npm run lint`. +- Viewer packages: run `npm run dev`, then `npm run test:ui`; inspect screenshots, + browser errors, keyboard behavior, narrow layout, and unchanged apply safeguards. +- Viewer build changes: `npm run build`. CLI/package changes: `npm run build:cli` + plus installation from the actual packed tarball, not only source tests. +- Format changes: old/new fixtures, parse/serialize round trips, unknown-field recovery + and installed-skill examples all agree. Never silently discard newer metadata. +- Add deterministic fail-before/pass-after coverage for the color/selection defects. + New capabilities get acceptance coverage rather than invented baseline failures. +- Use the Vault implement-verify harness, record unavailable checks, and perform final + source and behavior passes on unchanged code. Static checks alone do not finish UI work. +- Ship incrementally. Preserve old review files and fail closed on stale apply targets. + Preferences have safe defaults; scanner adapters remain opt-in until calibrated. + +## Spec Kit handoff + +Use this document as the requirements input to `/speckit.specify`, then +`/speckit.clarify`, `/speckit.plan`, `/speckit.tasks`, and `/speckit.analyze` for the +selected delivery package. Keep one feature workspace rather than creating a second +parallel plan. Read `~/.vault/skills/implement-verify/SKILL.md` before implementation. +The implementation consumed this approved roadmap directly with the Vault +implement-verify harness. No Spec Kit scaffolding or additional planning suite was +introduced. See `verification.md` for delivered scope and deviations. + +Recommended first implementation: WM-01, followed by WM-02. Have a fresh-context +reviewer check the resulting diff against the package's acceptance criteria; resolve +correctness gaps rather than expanding into stylistic cleanup. + +## Open decisions (proposed defaults allow planning to proceed) + +- Accept `info|normal|urgent|blocking` urgency, or choose a team priority vocabulary. +- Confirm that “multiple comments” means independent annotations initially; human + reply threads and browser editing remain later work unless explicitly requested. +- Accept compact collapsed notes as the default, with selection expanding one note. +- First installer targets: Codex and Claude Code; npm package stays public as currently + configured. Confirm separately before release if package access should change. +- Which repositories/languages should the first quality adapters support beyond this + TypeScript project? Start with TypeScript/ESLint; add language-specific adapters later. + +## External references checked for this plan + +- [npm package file allowlist and publishing configuration](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/). +- [Skills CLI private-repository authentication and local sources](https://github.com/vercel-labs/skills#private-repositories). +- [ESLint complexity rule](https://eslint.org/docs/latest/rules/complexity). +- [Knip setup and project configuration](https://knip.dev/overview/getting-started). +- [Semgrep Community Edition](https://semgrep.dev/products/community-edition/). + +These sources inform proposed integrations; no third-party tool installation or scan +was performed, and no measured code-accuracy claim is made. @note +1..290 kind=intent id=improvement-134 urgency: normal why: Keep the implementation and its acceptance criteria together so a later agent can continue the approved scope without reconstructing decisions. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: review "Compared documentation with implemented commands and the recorded scope" => pass @file specs/001-review-improvements/verification.md added +67 newsha=f1f7325 @@ -0,0 +1,67 @@ +# Review improvements — implementation evidence + +Date: 2026-09-17. Baseline: `28e5280` plus the previous task's uncommitted +AGENTS.md/CLAUDE.md and roadmap. No commits or publication. + +## Disposition + +| Package | Delivered boundary | Evidence | +| --- | --- | --- | +| WM-01 | Diff backgrounds survive category hover/focus; annotation selection uses outlines/gutters. | New browser regression failed before the fix: green `lab(... / 0.11)` became security red `oklch(... / 0.13)`. It passes after the fix. | +| WM-02 | All overlapping notes selectable; disclosure shows type, urgency and author confidence; compact rail keeps code rows contiguous. | Parser/round-trip tests; browser filtering, collapse/expand, precise side selection, layout and deep links. | +| WM-03 | Settings modal; bidirectional per-file scrolling, independent mode, clamping without snap-back; persistent preferences with session fallback. | Browser interaction, focus return, reload, unequal widths, independent files, storage write failure and 390px layout. | +| WM-04 | Bundled Codex/Claude skill installer with preview, conflict protection, repeatability and symlink rejection. | Unit tests plus actual npm tarballs installed offline into a clean temporary consumer; installed skill/reference read back. Skill validator passed using the existing specify-cli Python environment. | +| WM-05 | Claimed coverage distinguished from execution records; source/output identity, stale/unavailable/contradicted states; local locator checks. | Execution, mutation, stale test fixture, missing output, conflicting claims and imported-command tests. | +| WM-06 | ESLint JSON and configured checks, complexity/depth suggestions, import, clean-base comparison, expiring suppressions, cancellable watch mode and viewer findings. | Real ESLint process on a temporary repository; committed-base/shifted-line comparison; malformed paths/reports, timeouts/output bounds, cancellation, latest-only watch publication and no command execution during import. | + +## Verification commands + +Run in the repository, sequentially when the browser suite is involved: + +- `npm test` — 102 tests across 13 files, including package installation. +- `npm run typecheck` — passed. +- `npm run lint` — passed. +- `node tests/e2e/improvements.mjs` — new feature browser acceptance suite. +- `npm run test:ui` — 38 existing real-browser checks, including file/part apply, + stale-write protection, restoration of `examples/retry.ts`, and zero browser errors. +- `npm run build -- --webpack` — production build passed, including on Node 22.23.2. +- `git diff --check` — passed. +- `quick_validate.py .agents/skills/whymark` — passed with + `~/.local/share/uv/tools/specify-cli/bin/python` (its environment includes PyYAML). + +Node 22.23.2 and Node 24.20.0 were exercised on macOS. Chromium was used for UI +checks. Linux/Windows runtime behavior and live agent discovery were not exercised. +The source-only skill validator is distinct from packed installation and live discovery. +The generated handoff review records executable checks and source fingerprints. + +## Review and limitations + +Source pass: traced parser/serializer compatibility, frontend versus Node import +boundaries, code/apply part sharing, local path handling, evidence freshness, +installer destinations, and scanner cancellation/baseline semantics. +Behavior pass: negative fixtures and public CLI/browser boundaries above. These are +separate self-review passes, not an independent-agent audit. The execution records +in `reviews/review-improvements.whymark` bind the rerun checks to source bytes. + +- Urgency is canonically emitted as a body field, not a new header attribute: + the baseline v1 formatter demonstrably preserved `urgency: urgent` but drops + unknown header attributes. Updated readers accept both. +- Added `dist/**` to ESLint's generated-output exclusions; source remains linted. +- A first overlapping run of unit and browser tests saw the browser's temporary + retry-file edit and failed. Rerunning sequentially after restoration passed. + Do not run the mutating browser suite concurrently with unit tests. +- Default Turbopack build encountered environment failures (Google Fonts access, + then a subprocess port-binding restriction). The supported webpack build passed; + Turbopack production packaging is not claimed as verified in this environment. +- Evidence is unauthenticated, even when hashes match. Fingerprints exclude ignored + files, artifacts and review outputs under `reviews/`, and cannot prove dependency + bytes or external services. Bounds: 50,000 paths, 32 MB/file, 128 MB total; over-limit + source identity is unavailable rather than silently partial. +- Baseline comparison requires a clean relevant source checkout, matching resolved + base revision and identical tool commands/versions. No baseline means uncompared + findings. Duplicate identical diagnostics remain potentially ambiguous. +- First quality adapter delivery is ESLint plus configured pass/fail checks. Knip, + Semgrep, dependency-audit, coverage/SARIF adapters, authenticated attestations and + human reply threads remain later work, as proposed in the roadmap. +- npm installation works from the locally packed artifact. This version has not been + published; no global agent installation or deployment was performed. @note +1..67 kind=intent id=improvement-135 urgency: normal why: Keep the implementation and its acceptance criteria together so a later agent can continue the approved scope without reconstructing decisions. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: review "Compared documentation with implemented commands and the recorded scope" => pass @file src/cli/quality.ts added +79 newsha=45fbb6e @@ -0,0 +1,79 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import { parseWhymark } from "../lib/whymark/parse"; +import { serializeWhymark } from "../lib/whymark/serialize"; +import { repoRoot } from "../lib/whymark/git"; +import { gitHead, reviewFingerprint, sourceFingerprint, writeOutput } from "../lib/whymark/evidence-node"; +import { compareFindings } from "../lib/quality/baseline"; +import { parseBoundedJson, parseESLint, readQuality } from "../lib/quality/import"; +import { readConfig, scanQuality } from "../lib/quality/run"; +import type { QualityReport } from "../lib/quality/types"; + +interface Options { run: boolean; watch: boolean; write: boolean; config?: string; baseline?: string; importPath?: string; toolVersion?: string } +export async function qualityCommand(positionals: string[], options: Options) { + const cwd = repoRoot() || process.cwd(); + if (positionals[0] === "init") { + const path = join(cwd, "whymark.config.json"); + writeFileSync(path, `${JSON.stringify({ version: 1, checks: [ + { id: "eslint", format: "eslint", command: "node node_modules/eslint/bin/eslint.js . --format json --rule 'complexity: [warn, 15]' --rule 'max-depth: [warn, 4]'", versionCommand: "node node_modules/eslint/bin/eslint.js --version" }, + { id: "typecheck", format: "check", command: "npm run typecheck", versionCommand: "node node_modules/typescript/bin/tsc --version" }, + ], suppressions: [] }, null, 2)}\n`, { flag: "wx" }); + process.stdout.write(`Created ${path}. Review the commands before running quality --run.\n`); + return; + } + const path = positionals[0]; + if (!path || (options.run ? 1 : 0) + (options.importPath ? 1 : 0) !== 1 || options.watch && !options.run) { + throw new Error("Use quality --run or --import . See whymark help quality."); + } + let baseline: QualityReport | null = null; + if (options.baseline) { + baseline = readQuality(parseBoundedJson(readFileSync(options.baseline, "utf8"))); + if (!baseline) throw new Error("Invalid baseline report."); + } + let controller = new AbortController(); + let generation = 0; + const execute = async () => { + const mine = ++generation; + controller.abort(); controller = new AbortController(); + const signal = controller.signal; + const text = readFileSync(path, "utf8"); + const doc = parseWhymark(text); + let report: QualityReport; + if (options.importPath) { + const input = readFileSync(options.importPath, "utf8"); + report = compareFindings({ + version: 1, ran: new Date().toISOString(), source: null, sourceAfter: null, head: gitHead(cwd), clean: false, base: doc.meta.base ?? null, + reviewHash: reviewFingerprint(doc), provenance: "imported", comparison: "unavailable", + checks: [{ id: "eslint", version: options.toolVersion ?? "unknown", command: "Imported ESLint JSON (not executed)", status: "unavailable", exitCode: null, ...writeOutput(cwd, input), detail: "Execution and source state were not independently checked." }], + findings: parseESLint(input, name => relative(cwd, resolve(cwd, name)).split("\\").join("/")), + }, null); + } else report = await scanQuality(doc, cwd, readConfig(options.config ?? join(cwd, "whymark.config.json")), { baseline, signal }); + if (signal.aborted || mine !== generation) return; + if (options.write) { + if (readFileSync(path, "utf8") !== text) throw new Error("Review changed during the scan; results were not attached. Rerun against the current review."); + doc.meta.extra.quality = report; + writeFileSync(path, serializeWhymark(doc)); + } + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + if (!options.watch) process.exitCode = report.checks.some(check => check.status !== "pass") || report.findings.some(f => f.severity === "error" && f.status !== "resolved" && !f.suppression) ? 1 : 0; + }; + if (!options.watch) { await execute(); return; } + let previous = sourceFingerprint(cwd); + if (!previous) throw new Error("Cannot watch: repository fingerprint unavailable."); + let timer: ReturnType | undefined; + const launch = () => { void execute().catch(error => process.stderr.write(`${(error as Error).message}\n`)); }; + launch(); + const interval = setInterval(() => { + const next = sourceFingerprint(cwd); + if (next === previous) return; + previous = next; controller.abort(); generation++; + clearTimeout(timer); timer = setTimeout(launch, 400); + }, 1000); + await new Promise(done => { + const stop = () => { + clearInterval(interval); clearTimeout(timer); controller.abort(); generation++; + process.removeListener("SIGINT", stop); process.removeListener("SIGTERM", stop); done(); + }; + process.on("SIGINT", stop); process.on("SIGTERM", stop); + }); +} @note +1..79 kind=intent id=improvement-136 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/components/whymark/evidence-panel.tsx added +24 newsha=705d667 @@ -0,0 +1,24 @@ +import type { EvidenceView } from "@/lib/whymark/evidence"; + +export function EvidencePanel({ evidence }: { evidence: EvidenceView[] }) { + return ( +
+ Execution evidence · {evidence.length ? `${evidence.length} records` : "none recorded"} +

Author confidence and passing claims are not measured accuracy. Records are unauthenticated; source hashes establish freshness, not who ran a command.

+ {!evidence.length ?

No execution records. The percentages above count author claims.

: evidence.map(({ record, state, contradicted }, index) => ( +
+ + {contradicted ? "Contradicted claim · " : ""}{record.status} · {state === "current" ? "current source, output checked" : state === "imported" ? "imported, freshness unchecked" : state} · {record.command} + +
+
Recorded: {record.ran} · {record.durationMs}ms · exit {record.exitCode ?? "unavailable"}
+
{record.tool} · {record.runtime} · cwd {record.cwd}
+
{record.summary}
+
Source: {record.source ?? "unavailable"}
+
Output: {record.outputArtifact} · SHA-256 {record.outputHash}
+
+
+ ))} +
+ ); +} @note +1..24 kind=intent id=improvement-137 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @file src/components/whymark/note-disclosure.tsx added +13 newsha=37475bb @@ -0,0 +1,13 @@ +"use client"; + +import { createContext, useContext } from "react"; + +export const NoteDisclosure = createContext<{ expanded: (id: string) => boolean; toggle: (id: string) => void }>({ + expanded: () => false, + toggle: () => {}, +}); + +export function useNoteDisclosure(id: string) { + const state = useContext(NoteDisclosure); + return { expanded: state.expanded(id), toggle: () => state.toggle(id) }; +} @note +1..13 kind=intent id=improvement-138 urgency: normal why: Overlapping reasons must remain individually reachable without changing the meaning of added/deleted code. Independent disclosure and measured compact placement keep comment height from breaking the code reading flow. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @file src/components/whymark/quality-panel.tsx added +30 newsha=2a93736 @@ -0,0 +1,30 @@ +"use client"; + +import { useState } from "react"; +import type { QualityReport } from "@/lib/quality/types"; +import type { FileVM } from "@/lib/view-model"; + +export function QualityPanel({ report, state, files }: { report: QualityReport | null; state: "current" | "stale" | "unchecked"; files: FileVM[] }) { + const [changedOnly, setChangedOnly] = useState(false); + if (!report) return null; + const findings = report.findings.filter(finding => !changedOnly || finding.path === null || finding.line === null || files.some(file => file.path === finding.path && file.rows.some(row => row.kind === "add" && row.newLine !== undefined && row.newLine >= finding.line! && row.newLine <= (finding.endLine ?? finding.line!)))); + return ( +
+

Code quality · {state === "unchecked" ? "source unchecked" : `${state} source`}

+

{report.provenance === "imported" ? "Imported report; execution not checked." : "Recorded local checks; unauthenticated evidence."} Baseline comparison {report.comparison}. Static findings do not measure code accuracy.

+
    {report.checks.map(check =>
  • {check.id} {check.version}: {check.status}
    Command and output

    {check.command}
    {check.detail}
    {check.outputArtifact}
    SHA-256 {check.outputHash}

  • )}
+ +

{findings.length} of {report.findings.length} findings shown

+
    {findings.map((finding, index) => { + const file = files.findIndex(file => file.path === finding.path); + return
  • +
    {finding.severity}{finding.status}{finding.rule}
    +

    {finding.message}

    + {file >= 0 ? {finding.path}:{finding.line ?? "file"} : {finding.path ?? "project"}:{finding.line ?? "file"}} + {finding.helpUrl ? Rule documentation : null} + {finding.suppression ?

    Suppression: {finding.suppression.reason} · expires {finding.suppression.expires}

    : null} +
  • ; + })}
+
+ ); +} @note +1..30 kind=intent id=improvement-139 urgency: normal why: A passing author claim must not look like independently checked code. Present source freshness, imported provenance and concrete findings separately while keeping selection and keyboard navigation consistent. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm run typecheck` => pass (exit 0 in 1.5s) @file src/components/whymark/review-settings.tsx added +35 newsha=1691773 @@ -0,0 +1,35 @@ +"use client"; + +import { Settings } from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { DEFAULT_PREFERENCES, type ReviewPreferences } from "@/lib/review-preferences"; + +export function ReviewSettings({ value, onChange }: { + value: ReviewPreferences; + onChange: (value: ReviewPreferences) => void; +}) { + return ( + + + + + + Review settings + Preferences for this browser. Code decisions are kept separate. + {([ + ["syncScroll", "Synchronize horizontal scrolling"], + ["collapseNotes", "Collapse comments by default"], + ["compactRail", "Compact layout without blank code rows"], + ] as const).map(([key, label]) => ( + + ))} + + + + ); +} @note +1..35 kind=intent id=improvement-140 urgency: normal why: Reviewers need synchronized comparison without losing the option to inspect each pane independently. Browser preferences are isolated from code decisions and continue working when storage is blocked. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @file src/components/whymark/use-review-preferences.ts added +27 newsha=e2761ce @@ -0,0 +1,27 @@ +"use client"; + +import { useMemo, useSyncExternalStore } from "react"; +import { PREFERENCES_KEY, parsePreferences, type ReviewPreferences } from "@/lib/review-preferences"; + +let sessionValue: string | null = null; +let storageFailed = false; +const eventName = "whymark-preferences"; +function subscribe(notify: () => void) { + window.addEventListener("storage", notify); + window.addEventListener(eventName, notify); + return () => { window.removeEventListener("storage", notify); window.removeEventListener(eventName, notify); }; +} +function snapshot() { + if (storageFailed) return sessionValue; + try { return localStorage.getItem(PREFERENCES_KEY); } catch { storageFailed = true; return sessionValue; } +} +function update(value: ReviewPreferences) { + sessionValue = JSON.stringify(value); + try { localStorage.setItem(PREFERENCES_KEY, sessionValue); } catch { storageFailed = true; } + window.dispatchEvent(new Event(eventName)); +} +export function useReviewPreferences(): [ReviewPreferences, (value: ReviewPreferences) => void] { + const raw = useSyncExternalStore(subscribe, snapshot, () => null); + const value = useMemo(() => parsePreferences(raw), [raw]); + return [value, update]; +} @note +1..27 kind=intent id=improvement-141 urgency: normal why: Reviewers need synchronized comparison without losing the option to inspect each pane independently. Browser preferences are isolated from code decisions and continue working when storage is blocked. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @file src/lib/quality/baseline.ts added +39 newsha=d41bb50 @@ -0,0 +1,39 @@ +import { sha256 } from "../whymark/evidence-node"; +import type { QualityFinding, QualityReport } from "./types"; + +export function identifyFindings(findings: QualityFinding[]): QualityFinding[] { + const counts = new Map(); + return findings.map(finding => { + const key = JSON.stringify([finding.tool, finding.rule, finding.path, finding.message]); + const occurrence = counts.get(key) ?? 0; + counts.set(key, occurrence + 1); + return { ...finding, id: sha256(`${key}:${occurrence}`) }; + }); +} + +export function compareFindings(current: QualityReport, baseline: QualityReport | null, renames: Map = new Map()): QualityReport { + const usable = baseline && baseline.clean && current.base && baseline.head && + current.base.endsWith(baseline.head) && baseline.source === baseline.sourceAfter && baseline.source !== null && + current.source === current.sourceAfter && current.source !== null && + current.checks.every(check => check.status !== "unavailable" && baseline.checks.some(old => old.id === check.id && old.version === check.version && old.command === check.command && old.status !== "unavailable")); + if (!usable) return { ...current, comparison: "unavailable", findings: identifyFindings(current.findings).map(finding => ({ ...finding, status: "uncompared" })) }; + const old = identifyFindings(baseline.findings.filter(finding => finding.status !== "resolved").map(finding => ({ ...finding, path: finding.path ? renames.get(finding.path) ?? finding.path : null }))); + const pending = new Map(old.map(finding => [finding.id, finding])); + const findings: QualityFinding[] = identifyFindings(current.findings).map(finding => { + const existing = pending.delete(finding.id); + return { ...finding, status: existing ? "existing" as const : "new" as const }; + }); + for (const finding of pending.values()) { + if (current.checks.some(check => check.id === finding.tool)) findings.push({ ...finding, status: "resolved" }); + } + return { ...current, comparison: "available", findings }; +} + +export function applySuppressions(findings: QualityFinding[], suppressions: Array<{ id: string; reason: string; expires: string }>, now = Date.now()): QualityFinding[] { + return findings.map(finding => { + const match = suppressions.find(item => item.id === finding.id && item.reason.trim() && Date.parse(item.expires) > now); + const clean = { ...finding }; + delete clean.suppression; + return match ? { ...clean, suppression: { reason: match.reason, expires: match.expires } } : clean; + }); +} @note +1..39 kind=intent id=improvement-142 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/quality/import.ts added +62 newsha=e31f39d @@ -0,0 +1,62 @@ +import type { QualityFinding, QualityReport } from "./types"; + +const MAX_BYTES = 8 * 1024 * 1024; +export function parseBoundedJson(text: string): unknown { + if (new TextEncoder().encode(text).length > MAX_BYTES) throw new Error("Quality report exceeds 8 MB."); + return JSON.parse(text); +} + +export function safePath(path: string): boolean { + return path.length > 0 && !path.startsWith("/") && !/^[A-Za-z]:/.test(path) && !path.includes("\\") && !path.split("/").includes("..") && !/[\0-\x1f]/.test(path); +} + +export function readQuality(value: unknown): QualityReport | null { + if (!value || typeof value !== "object") return null; + const r = value as QualityReport; + const hash = (v: unknown) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v); + const nullableHash = (v: unknown) => v === null || hash(v); + if (r.version !== 1 || typeof r.clean !== "boolean" || typeof r.ran !== "string" || !Number.isFinite(Date.parse(r.ran)) || + !nullableHash(r.source) || !nullableHash(r.sourceAfter) || !hash(r.reviewHash) || + !(r.head === null || typeof r.head === "string") || !(r.base === null || typeof r.base === "string") || + !["local-run", "imported"].includes(r.provenance) || !["available", "unavailable"].includes(r.comparison) || + !Array.isArray(r.checks) || r.checks.length > 100 || !Array.isArray(r.findings) || r.findings.length > 20000) return null; + if (!r.checks.every(c => c && typeof c.id === "string" && typeof c.version === "string" && + typeof c.command === "string" && ["pass", "fail", "unavailable"].includes(c.status) && + (c.exitCode === null || Number.isInteger(c.exitCode)) && hash(c.outputHash) && + typeof c.outputArtifact === "string" && typeof c.detail === "string")) return null; + if (!r.findings.every(f => f && typeof f.id === "string" && typeof f.tool === "string" && + typeof f.rule === "string" && typeof f.message === "string" && + ["warning", "error"].includes(f.severity) && (f.path === null || typeof f.path === "string" && safePath(f.path)) && + (f.line === null || Number.isInteger(f.line) && f.line > 0) && + (f.endLine === null || Number.isInteger(f.endLine) && f.endLine >= (f.line ?? 1)) && + (f.helpUrl === undefined || typeof f.helpUrl === "string" && /^https?:\/\//.test(f.helpUrl)) && + ["new", "existing", "resolved", "uncompared"].includes(f.status) && + (f.suppression === undefined || f.suppression !== null && typeof f.suppression === "object" && typeof f.suppression.reason === "string" && !!f.suppression.reason.trim() && typeof f.suppression.expires === "string" && Number.isFinite(Date.parse(f.suppression.expires))))) return null; + return r; +} + +export interface ESLintFile { + filePath: string; + messages: Array<{ ruleId: string | null; severity: number; message: string; line?: number; endLine?: number }>; +} + +export function parseESLint(text: string, relativePath: (path: string) => string): QualityFinding[] { + const input = parseBoundedJson(text); + if (!Array.isArray(input) || input.length > 20000) throw new Error("Expected an ESLint JSON array."); + const findings: QualityFinding[] = []; + for (const file of input) { + if (!file || typeof file.filePath !== "string" || !Array.isArray(file.messages)) throw new Error("Malformed ESLint file result."); + const path = relativePath(file.filePath); + if (!safePath(path)) throw new Error("ESLint result path escapes the repository."); + for (const message of file.messages) { + if (!message || typeof message.message !== "string" || ![1, 2].includes(message.severity) || !(message.ruleId === null || typeof message.ruleId === "string")) throw new Error("Malformed ESLint message."); + if (message.line !== undefined && (!Number.isInteger(message.line) || message.line < 1)) throw new Error("Invalid finding line."); + if (message.endLine !== undefined && (!Number.isInteger(message.endLine) || message.endLine < (message.line ?? 1))) throw new Error("Invalid finding range."); + findings.push({ id: "", tool: "eslint", rule: message.ruleId ?? "parse-error", severity: message.severity === 2 ? "error" : "warning", message: message.message, path, line: message.line ?? null, endLine: message.endLine ?? message.line ?? null, status: "uncompared", + ...(message.ruleId && /^[a-z-]+$/.test(message.ruleId) ? { helpUrl: `https://eslint.org/docs/latest/rules/${message.ruleId}` } : {}), + }); + if (findings.length > 20000) throw new Error("Too many quality findings."); + } + } + return findings; +} @note +1..62 kind=intent id=improvement-143 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/quality/run.ts added +96 newsha=695d628 @@ -0,0 +1,96 @@ +import { spawn } from "node:child_process"; +import { execFileSync } from "node:child_process"; +import { readFileSync, realpathSync } from "node:fs"; +import { relative, resolve } from "node:path"; +import type { WhymarkDocument } from "../whymark/types"; +import { cleanSource, resolveRevision, gitHead, reviewFingerprint, sourceFingerprint, writeOutput } from "../whymark/evidence-node"; +import { applySuppressions, compareFindings } from "./baseline"; +import { parseBoundedJson, parseESLint } from "./import"; +import type { QualityReport } from "./types"; + +export interface QualityConfig { + version: 1; + checks: Array<{ id: string; command: string; versionCommand: string; format: "eslint" | "check" }>; + suppressions?: Array<{ id: string; reason: string; expires: string }>; +} + +export function readConfig(path: string): QualityConfig { + const value = parseBoundedJson(readFileSync(path, "utf8")) as QualityConfig; + if (!value || value.version !== 1 || !Array.isArray(value.checks) || !value.checks.length || value.checks.length > 20 || + !value.checks.every(c => c && typeof c.id === "string" && /^[a-z][a-z0-9-]*$/.test(c.id) && typeof c.command === "string" && c.command.length > 0 && c.command.length < 10000 && typeof c.versionCommand === "string" && c.versionCommand.length > 0 && c.versionCommand.length < 10000 && ["eslint", "check"].includes(c.format)) || + new Set(value.checks.map(c => c.id)).size !== value.checks.length) throw new Error("Invalid whymark.config.json checks."); + if (value.suppressions !== undefined && (!Array.isArray(value.suppressions) || !value.suppressions.every(s => s && typeof s.id === "string" && typeof s.reason === "string" && s.reason.trim() && typeof s.expires === "string" && Number.isFinite(Date.parse(s.expires))))) throw new Error("Invalid quality suppressions."); + return value; +} + +export function runTool(command: string, cwd: string, signal?: AbortSignal, timeoutMs = 120000): Promise<{ code: number | null; stdout: string; stderr: string; unavailable: boolean }> { + return new Promise(resolveResult => { + if (signal?.aborted) { resolveResult({ code: null, stdout: "", stderr: "Cancelled", unavailable: true }); return; } + const child = spawn(command, { cwd, shell: true, detached: process.platform !== "win32", env: { ...process.env, CI: "1", NO_COLOR: "1" }, stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; let stderr = ""; let size = 0; let unavailable = false; + const stop = (reason: string) => { + unavailable = true; + stderr += `\n${reason}`; + try { if (child.pid && process.platform !== "win32") process.kill(-child.pid, "SIGKILL"); else child.kill("SIGKILL"); } catch { /* Already exited. */ } + }; + const abort = () => stop("Cancelled"); + signal?.addEventListener("abort", abort, { once: true }); + const timeout = setTimeout(() => stop("Timed out"), timeoutMs); + child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); + const append = (text: string, error: boolean) => { + size += Buffer.byteLength(text); + if (size > 8 * 1024 * 1024) { if (!unavailable) stop("Output exceeds 8 MB"); return; } + if (error) stderr += text; else stdout += text; + }; + child.stdout.on("data", text => append(text, false)); + child.stderr.on("data", text => append(text, true)); + child.on("error", error => { unavailable = true; stderr += error.message; }); + child.on("close", code => { + clearTimeout(timeout); signal?.removeEventListener("abort", abort); + resolveResult({ code, stdout, stderr, unavailable: unavailable || code === null || code === 127 }); + }); + }); +} + +export async function scanQuality(doc: WhymarkDocument, cwd: string, config: QualityConfig, options: { baseline?: QualityReport | null; signal?: AbortSignal } = {}): Promise { + cwd = realpathSync(cwd); + const report: QualityReport = { + version: 1, ran: new Date().toISOString(), source: sourceFingerprint(cwd), sourceAfter: null, + head: gitHead(cwd), clean: cleanSource(cwd), base: resolveRevision(cwd, doc.meta.base), reviewHash: reviewFingerprint(doc), + provenance: "local-run", comparison: "unavailable", checks: [], findings: [], + }; + for (const check of config.checks) { + if (options.signal?.aborted) break; + const version = await runTool(check.versionCommand, cwd, options.signal, 10000); + const run = await runTool(check.command, cwd, options.signal); + let unavailable = run.unavailable || version.unavailable || version.code !== 0; + let detail = run.stderr.trim().slice(0, 1000); + if (check.format === "eslint" && !unavailable) { + if (run.code !== 0 && run.code !== 1) unavailable = true; + else try { + const findings = parseESLint(run.stdout, path => relative(cwd, resolve(cwd, path)).split("\\").join("/")); + if (run.code === 1 && !findings.length) throw new Error("ESLint failed without findings."); + report.findings.push(...findings.map(finding => ({ ...finding, tool: check.id }))); + } catch (error) { unavailable = true; detail = (error as Error).message; } + } + report.checks.push({ id: check.id, version: version.stdout.trim().slice(0, 200) || "unavailable", command: check.command, + status: unavailable ? "unavailable" : run.code === 0 ? "pass" : "fail", exitCode: run.code, + ...writeOutput(cwd, `${run.stdout}\n${run.stderr}`), detail, + }); + } + report.sourceAfter = sourceFingerprint(cwd); + const renames = new Map(); + const base = options.baseline?.head; + if (base && /^[a-f0-9]{40}$/.test(base)) { + try { + const tokens = execFileSync("git", ["diff", "--name-status", "-z", "--find-renames", base, "--"], { cwd, encoding: "utf8" }).split("\0"); + for (let i = 0; i < tokens.length;) { + const status = tokens[i++]; const old = tokens[i++]; + if (status?.startsWith("R")) renames.set(old, tokens[i++]); + } + } catch { /* Baseline comparison remains explicit. */ } + } + const compared = compareFindings(report, options.baseline ?? null, renames); + compared.findings = applySuppressions(compared.findings, config.suppressions ?? []); + return compared; +} @note +1..96 kind=intent id=improvement-144 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/quality/types.ts added +39 newsha=1668bd2 @@ -0,0 +1,39 @@ +export interface QualityFinding { + id: string; + tool: string; + rule: string; + severity: "warning" | "error"; + message: string; + path: string | null; + line: number | null; + endLine: number | null; + helpUrl?: string; + status: "new" | "existing" | "resolved" | "uncompared"; + suppression?: { reason: string; expires: string }; +} + +export interface QualityCheck { + id: string; + version: string; + command: string; + status: "pass" | "fail" | "unavailable"; + exitCode: number | null; + outputHash: string; + outputArtifact: string; + detail: string; +} + +export interface QualityReport { + version: 1; + ran: string; + source: string | null; + sourceAfter: string | null; + head: string | null; + clean: boolean; + base: string | null; + reviewHash: string; + provenance: "local-run" | "imported"; + comparison: "available" | "unavailable"; + checks: QualityCheck[]; + findings: QualityFinding[]; +} @note +1..39 kind=intent id=improvement-145 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/review-preferences.ts added +29 newsha=69b6c70 @@ -0,0 +1,29 @@ +export interface ReviewPreferences { + version: 1; + syncScroll: boolean; + collapseNotes: boolean; + compactRail: boolean; +} + +export const DEFAULT_PREFERENCES: ReviewPreferences = { + version: 1, + syncScroll: true, + collapseNotes: true, + compactRail: true, +}; +export const PREFERENCES_KEY = "whymark.preferences.v1"; + +export function parsePreferences(raw: string | null): ReviewPreferences { + try { + const value = JSON.parse(raw ?? "null"); + if (value?.version !== 1) return { ...DEFAULT_PREFERENCES }; + return { + version: 1, + syncScroll: typeof value.syncScroll === "boolean" ? value.syncScroll : true, + collapseNotes: typeof value.collapseNotes === "boolean" ? value.collapseNotes : true, + compactRail: typeof value.compactRail === "boolean" ? value.compactRail : true, + }; + } catch { + return { ...DEFAULT_PREFERENCES }; + } +} @note +1..29 kind=intent id=improvement-146 urgency: normal why: Reviewers need synchronized comparison without losing the option to inspect each pane independently. Browser preferences are isolated from code decisions and continue working when storage is blocked. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @file src/lib/skill/install.ts added +67 newsha=e401642 @@ -0,0 +1,67 @@ +import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { homedir } from "node:os"; + +export interface InstallOptions { + packageRoot: string; + cwd: string; + agents?: string[]; + global?: boolean; + home?: string; + force?: boolean; + dryRun?: boolean; +} + +export function installSkill(options: InstallOptions): Array<{ path: string; action: string }> { + const agents = [...new Set(options.agents?.length ? options.agents : ["codex"])]; + if (agents.some(agent => !["codex", "claude-code"].includes(agent))) { + throw new Error("Supported agents: codex, claude-code."); + } + const base = realpathSync(options.global ? options.home ?? homedir() : options.cwd); + const skill = readFileSync(join(options.packageRoot, ".agents/skills/whymark/SKILL.md"), "utf8") + .replaceAll("spec/whymark-v1.md", "references/whymark-v1.md"); + const assets = [ + ["SKILL.md", skill], + ["references/whymark-v1.md", readFileSync(join(options.packageRoot, "spec/whymark-v1.md"), "utf8")], + ]; + const pending: Array<{ path: string; content: string; action: string }> = []; + for (const agent of agents) { + const destination = join(base, agent === "codex" ? ".agents" : ".claude", "skills/whymark"); + for (const [name, content] of assets) { + const path = resolve(destination, name); + assertNoSymlinks(base, path); + const identical = existsSync(path) && readFileSync(path, "utf8") === content; + if (!identical && existsSync(path) && !options.force) { + throw new Error(`Refusing to overwrite ${path}. Use --force to replace this skill explicitly.`); + } + pending.push({ path, content, action: identical ? "unchanged" : existsSync(path) ? "replace" : "create" }); + } + } + if (!options.dryRun) { + for (const file of pending) { + if (file.action === "unchanged") continue; + assertNoSymlinks(base, file.path); + mkdirSync(dirname(file.path), { recursive: true }); + writeFileSync(file.path, file.content, { flag: file.action === "create" ? "wx" : "w" }); + } + } + return pending.map(({ path, action }) => ({ path, action })); +} + +function assertNoSymlinks(base: string, path: string) { + const parts = relative(base, path).split(sep); + if (parts.includes("..")) throw new Error("Skill destination escapes its installation directory."); + let current = base; + for (const [index, part] of parts.entries()) { + current = join(current, part); + let info; + try { info = lstatSync(current); } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") continue; + throw error; + } + if (info.isSymbolicLink()) throw new Error(`Refusing symlink destination ${current}.`); + if (index < parts.length - 1 ? !info.isDirectory() : !info.isFile()) { + throw new Error(`Unexpected destination type at ${current}.`); + } + } +} @note +1..67 kind=intent id=improvement-147 urgency: normal why: Users of the npm artifact cannot depend on access to the private source repository. Resolve bundled resources from the installed package, expose explicit installation and scan commands, and preserve existing CLI aliases and customized agent files. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/evidence-node.ts added +101 newsha=fcf8ad7 @@ -0,0 +1,101 @@ +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { lstatSync, readFileSync, readlinkSync, realpathSync, mkdirSync, writeFileSync } from "node:fs"; +import { join, relative, resolve, sep } from "node:path"; +import type { WhymarkDocument } from "./types"; +import { evidenceViews, type ExecutionEvidence } from "./evidence"; + +export const sha256 = (text: string | Buffer) => createHash("sha256").update(text).digest("hex"); + +export function gitHead(cwd: string): string | null { + try { return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch { return null; } +} + +function excluded(path: string) { + return (path.startsWith("reviews/") && (path.endsWith(".whymark") || path.endsWith(".quality.json"))) || path.startsWith(".artifacts/"); +} + +export function cleanSource(cwd: string): boolean { + try { + const entries = execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd, encoding: "utf8" }).split("\0").filter(Boolean); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (!excluded(entry.slice(3))) return false; + if (/[RC]/.test(entry.slice(0, 2)) && !excluded(entries[++i] ?? "")) return false; + } + return true; + } catch { return false; } +} + +export function resolveRevision(cwd: string, value: string | undefined): string | null { + const hash = value?.match(/(?:^|@)([a-f0-9]{7,40})$/)?.[1]; + if (!hash) return null; + try { return execFileSync("git", ["rev-parse", "--verify", `${hash}^{commit}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch { return null; } +} + +export function sourceFingerprint(cwd: string): string | null { + try { + const names = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); + const paths = [...new Set(names.split("\0").filter(Boolean))].sort(); + if (paths.length > 50000) return null; + const digest = createHash("sha256").update("whymark-source-v1\0").update(gitHead(cwd) ?? "unborn"); + let bytes = 0; + for (const path of paths) { + if (excluded(path)) continue; + digest.update(`\0${path}\0`); + try { + const info = lstatSync(join(cwd, path)); + digest.update(String(info.mode)); + if (info.isSymbolicLink()) digest.update(`link:${readlinkSync(join(cwd, path))}`); + else if (info.isFile()) { + bytes += info.size; + if (info.size > 32 * 1024 * 1024 || bytes > 128 * 1024 * 1024) return null; + digest.update(readFileSync(join(cwd, path))); + } else return null; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") return null; + digest.update("missing"); + } + } + return digest.digest("hex"); + } catch { return null; } +} + +export function reviewFingerprint(doc: WhymarkDocument): string { + return sha256(JSON.stringify({ base: doc.meta.base, head: doc.meta.head, files: doc.files.map(file => ({ path: file.path, oldSha: file.oldSha, newSha: file.newSha, hunks: file.hunks.map(hunk => ({ oldStart: hunk.oldStart, newStart: hunk.newStart, heading: hunk.heading, lines: hunk.lines })) })) })); +} + +export function writeOutput(cwd: string, output: string): { outputHash: string; outputArtifact: string } { + const outputHash = sha256(output); + const outputArtifact = `.artifacts/whymark/${outputHash}.log`; + const root = realpathSync(cwd); + const directory = join(root, ".artifacts/whymark"); + for (const part of [join(root, ".artifacts"), directory]) { + try { if (lstatSync(part).isSymbolicLink()) throw new Error("Evidence artifact directory is a symlink."); } + catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } + } + mkdirSync(directory, { recursive: true }); + const path = join(root, outputArtifact); + try { writeFileSync(path, output, { flag: "wx" }); } + catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EEXIST" || lstatSync(path).isSymbolicLink() || sha256(readFileSync(path)) !== outputHash) throw error; + } + return { outputHash, outputArtifact }; +} + +export function localEvidence(doc: WhymarkDocument, cwd: string) { + return evidenceViews(doc, { + source: sourceFingerprint(cwd), reviewHash: reviewFingerprint(doc), + outputAvailable: (record: ExecutionEvidence) => { + try { + if (!/^\.artifacts\/whymark\/[a-f0-9]{64}\.log$/.test(record.outputArtifact)) return false; + const root = realpathSync(cwd); + const path = realpathSync(resolve(root, record.outputArtifact)); + const rel = relative(root, path); + if (rel === ".." || rel.startsWith(`..${sep}`)) return false; + if (lstatSync(path).size > 32 * 1024 * 1024) return false; + return sha256(readFileSync(path)) === record.outputHash; + } catch { return false; } + }, + }); +} @note +1..101 kind=intent id=improvement-148 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file src/lib/whymark/evidence.ts added +68 newsha=e7304d6 @@ -0,0 +1,68 @@ +import type { WhymarkDocument, VerifyStatus } from "./types"; + +export interface ExecutionEvidence { + version: 1; + command: string; + noteId: string | null; + file: string | null; + claimed: VerifyStatus; + status: "pass" | "fail" | "unavailable"; + ran: string; + durationMs: number; + exitCode: number | null; + cwd: string; + tool: string; + runtime: string; + source: string | null; + sourceAfter: string | null; + head: string | null; + base: string | null; + reviewHash: string; + outputHash: string; + outputArtifact: string; + summary: string; +} + +export type EvidenceState = "imported" | "current" | "stale" | "unavailable"; +export interface EvidenceView { + record: ExecutionEvidence; + state: EvidenceState; + contradicted: boolean; +} + +const hash = (value: unknown) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value); +const nullableString = (value: unknown) => value === null || typeof value === "string"; +const status = (value: unknown) => ["pass", "fail", "unknown", "skipped"].includes(String(value)); + +export function readEvidence(value: unknown): ExecutionEvidence[] { + if (!Array.isArray(value) || value.length > 1000) return []; + return value.filter((r): r is ExecutionEvidence => r && typeof r === "object" && + r.version === 1 && typeof r.command === "string" && r.command.length <= 10000 && + nullableString(r.noteId) && nullableString(r.file) && status(r.claimed) && + ["pass", "fail", "unavailable"].includes(r.status) && + typeof r.ran === "string" && Number.isFinite(Date.parse(r.ran)) && + Number.isFinite(r.durationMs) && r.durationMs >= 0 && + (r.exitCode === null || Number.isInteger(r.exitCode)) && + (r.status !== "pass" || r.exitCode === 0) && + typeof r.cwd === "string" && typeof r.tool === "string" && typeof r.runtime === "string" && + (r.source === null || hash(r.source)) && (r.sourceAfter === null || hash(r.sourceAfter)) && + nullableString(r.head) && nullableString(r.base) && hash(r.reviewHash) && hash(r.outputHash) && + typeof r.outputArtifact === "string" && typeof r.summary === "string"); +} + +export function evidenceViews(doc: WhymarkDocument, local?: { + source: string | null; + reviewHash: string; + outputAvailable: (record: ExecutionEvidence) => boolean; +}): EvidenceView[] { + return readEvidence(doc.meta.extra.evidence).map(record => { + let state: EvidenceState = "imported"; + if (record.status === "unavailable" || !record.source || !record.sourceAfter) state = "unavailable"; + else if (record.source !== record.sourceAfter) state = "stale"; + else if (local) { + if (!local.source || !local.outputAvailable(record)) state = "unavailable"; + else state = record.source === local.source && record.reviewHash === local.reviewHash ? "current" : "stale"; + } + return { record, state, contradicted: (record.claimed === "pass" || record.claimed === "fail") && record.status !== "unavailable" && record.status !== record.claimed }; + }); +} @note +1..68 kind=intent id=improvement-149 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/e2e/improvements.mjs added +111 newsha=a03f99a @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +import { copyFileSync, mkdirSync, rmSync } from "node:fs"; +import { chromium } from "playwright"; + +const base = process.env.WHYMARK_URL ?? "http://127.0.0.1:43917"; +const out = process.env.WHYMARK_ARTIFACTS ?? "/tmp/whymark-improvements"; +const fixture = "reviews/ui-improvements-fixture.whymark"; +mkdirSync(out, { recursive: true }); +copyFileSync("tests/fixtures/multiple-notes.whymark", fixture); +const browser = await chromium.launch(); +try { + const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); + await page.goto(`${base}/r/ui-improvements-fixture`, { waitUntil: "networkidle" }); + const row = page.locator('[data-whymark-body="unified"] .whymark-row[data-notes~="security"]').first(); + const background = await row.evaluate(el => getComputedStyle(el).backgroundColor); + await page.locator('#note-security').hover(); + await page.screenshot({ path: `${out}/hover.png` }); + assert.equal(await row.evaluate(el => getComputedStyle(el).backgroundColor), background, "security hover must preserve addition background"); + console.log("PASS semantic hover colors"); + assert.equal(await page.locator('#note-security button[aria-expanded]').getAttribute('aria-expanded'), 'false'); + assert.match(await page.locator('#note-security').innerText(), /urgent/); + assert.match(await page.locator('#note-security').innerText(), /80% confidence/); + await row.getByRole('button', { name: '3 comments on line 1' }).click(); + const chooser = page.getByRole('navigation', { name: 'Comments on selected line' }); + assert.equal(await chooser.getByRole('button').count(), 3); + await chooser.getByRole('button', { name: /intent/ }).click(); + assert.equal(await page.locator('#note-intent button[aria-expanded]').getAttribute('aria-expanded'), 'true'); + await page.getByRole('button', { name: 'Collapse comment intent' }).click(); + assert.equal(await page.locator('#note-intent button[aria-expanded]').getAttribute('aria-expanded'), 'false'); + await page.getByRole('button', { name: 'Expand all', exact: true }).click(); + assert.equal(await page.locator('[aria-expanded="false"][aria-controls^="comment-body-"]').count(), 0); + assert.equal(await page.locator('.whymark-gap').count(), 0); + await page.waitForFunction(() => { + const rects = [...document.querySelectorAll('section#file-0 [id^="note-"]')].map(card => card.getBoundingClientRect()).sort((a,b) => a.top-b.top); + return rects.every((rect, i) => !i || rect.top >= rects[i-1].bottom - 1); + }); + const overlap = await page.locator('section#file-0 [id^="note-"]').evaluateAll(cards => { + const rects = cards.map(card => card.getBoundingClientRect()).sort((a, b) => a.top - b.top); + return rects.some((rect, i) => i && rect.top < rects[i - 1].bottom - 1); + }); + assert.equal(overlap, false, 'expanded cards must not overlap'); + await page.getByRole('button', { name: 'Collapse all', exact: true }).click(); + await page.getByRole('button', { name: /^intent\s*1$/ }).click(); + const filtered = page.locator('[data-whymark-body="unified"] .whymark-row[data-notes~="intent"]').first(); + assert.equal(await filtered.getAttribute('data-notes'), 'intent'); + await filtered.getByRole('button', { name: '1 comments on line 1' }).click(); + assert.equal(await page.locator('#note-intent button[aria-expanded]').getAttribute('aria-expanded'), 'true'); + await page.getByRole('button', { name: /^clear$/ }).click(); + console.log('PASS multiple comments, filtering, disclosure and compact layout'); + await page.getByRole('button', { name: /^split$/ }).click(); + const left = page.locator('[data-whymark-side="del"]').first(); + const right = page.locator('[data-whymark-side="add"]').first(); + await left.evaluate(el => { el.scrollLeft = 120; }); + await page.waitForTimeout(100); + assert.equal(await right.evaluate(el => el.scrollLeft), 120); + await right.evaluate(el => { el.scrollLeft = 60; }); + await page.waitForTimeout(100); + assert.equal(await left.evaluate(el => el.scrollLeft), 60); + const settings = page.getByRole('button', { name: 'Review settings', exact: true }); + await settings.click(); + await page.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).uncheck(); + await page.keyboard.press('Escape'); + await page.waitForFunction(() => document.activeElement?.getAttribute('aria-label') === 'Review settings'); + assert.equal(await settings.evaluate(el => el === document.activeElement), true); + await left.evaluate(el => { el.scrollLeft = 200; }); + await page.waitForTimeout(100); + assert.equal(await right.evaluate(el => el.scrollLeft), 60); + await settings.click(); + await page.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).check(); + await page.keyboard.press('Escape'); + await page.waitForTimeout(100); + assert.equal(await right.evaluate(el => el.scrollLeft), 200); + const secondRight = page.locator('[data-whymark-side="add"]').nth(1); + const secondLeft = page.locator('[data-whymark-side="del"]').nth(1); + await secondRight.evaluate(el => { el.scrollLeft = 250; }); + await page.waitForTimeout(100); + assert.equal(await secondLeft.evaluate(el => el.scrollLeft), 0); + assert.equal(await secondRight.evaluate(el => el.scrollLeft), 250, 'short peer must not snap origin backwards'); + assert.equal(await right.evaluate(el => el.scrollLeft), 200, 'files must scroll independently'); + await page.screenshot({ path: `${out}/split.png` }); + await settings.click(); + await page.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).uncheck(); + await page.keyboard.press('Escape'); + await page.reload({ waitUntil: 'networkidle' }); + await settings.click(); + assert.equal(await page.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).isChecked(), false); + await page.getByRole('button', { name: 'Reset defaults' }).click(); + await page.keyboard.press('Escape'); + console.log('PASS bidirectional split sync, setting, persistence and focus return'); + await page.setViewportSize({ width: 390, height: 844 }); + await page.waitForTimeout(300); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'no page overflow on narrow screens'); + assert.equal(await page.locator('#note-security').count(), 1); + await page.screenshot({ path: `${out}/narrow.png` }); + await page.goto(`${base}/r/ui-improvements-fixture#note-intent`, { waitUntil: 'networkidle' }); + assert.equal(await page.locator('#note-intent button[aria-expanded]').getAttribute('aria-expanded'), 'true'); + console.log('PASS narrow layout and deep links'); + const blocked = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); + await blocked.addInitScript(() => { Storage.prototype.setItem = () => { throw new DOMException('Quota exceeded', 'QuotaExceededError'); }; }); + await blocked.goto(`${base}/r/ui-improvements-fixture`, { waitUntil: 'networkidle' }); + await blocked.getByRole('button', { name: 'Review settings', exact: true }).click(); + await blocked.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).uncheck(); + assert.equal(await blocked.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).isChecked(), false); + await blocked.keyboard.press('Escape'); + await blocked.getByRole('button', { name: 'Review settings', exact: true }).click(); + assert.equal(await blocked.getByRole('checkbox', { name: 'Synchronize horizontal scrolling' }).isChecked(), false); + console.log('PASS storage write failure retains session preferences'); +} finally { + await browser.close(); + rmSync(fixture, { force: true }); +} @note +1..111 kind=intent id=improvement-150 urgency: normal why: Exercise observable behavior and failure boundaries rather than only matching implementation text: overlapping selection, reversible file decisions, evidence freshness, unsafe destinations and scanner cancellation must remain reliable. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `node tests/e2e/improvements.mjs` => pass (exit 0 in 6.7s) @file tests/evidence.test.ts added +81 newsha=4727a1a @@ -0,0 +1,81 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { parseWhymark } from "../src/lib/whymark/parse"; +import { serializeWhymark } from "../src/lib/whymark/serialize"; +import { verifyDocument } from "../src/lib/whymark/verify"; +import { evidenceViews, readEvidence } from "../src/lib/whymark/evidence"; +import { localEvidence, sourceFingerprint } from "../src/lib/whymark/evidence-node"; +import { hasPassingVerify } from "../src/lib/whymark/stats"; +import { collectSourceDiagnostics } from "../src/lib/whymark/validate-tree"; +const dirs: string[] = []; +function repo() { + const cwd = mkdtempSync(join(tmpdir(), "whymark-evidence-")); dirs.push(cwd); + execFileSync("git", ["init", "-q"], { cwd }); + mkdirSync(join(cwd, "reviews")); mkdirSync(join(cwd, "tests")); + writeFileSync(join(cwd, "file.ts"), "const a = 1;\n"); + writeFileSync(join(cwd, "tests/fixture.whymark"), "test input"); + return cwd; +} +afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); +const input = `--- +whymark: 1 +title: evidence +checks: + - cmd: node -e "console.log('checked')" + status: pass +--- +@file file.ts added +1 +@@ -0,0 +1 @@ ++const a = 1; +@note +1 id=reason +why: Preserve an explicit local value. +verify: cmd \`node -e "process.exit(1)"\` => pass +`; + +describe("verification evidence", () => { + it("records output and source identity, preserves contradiction, detects stale tests", () => { + const cwd = repo(); const doc = parseWhymark(input); + const results = verifyDocument(doc, { cwd, recordEvidence: true, toolVersion: "test" }); + expect(results.map(r => r.outcome)).toEqual(["confirmed", "contradicted"]); + const serialized = serializeWhymark(doc); + writeFileSync(join(cwd, "reviews/test.whymark"), serialized); + const reread = parseWhymark(serialized); + expect(readEvidence(reread.meta.extra.evidence)).toHaveLength(2); + expect(localEvidence(reread, cwd).map(r => r.state)).toEqual(["current", "current"]); + expect(localEvidence(reread, cwd)[1].contradicted).toBe(true); + expect(evidenceViews(reread)[0].state).toBe("imported"); + writeFileSync(join(cwd, "tests/fixture.whymark"), "changed test input"); + expect(localEvidence(reread, cwd).every(r => r.state === "stale")).toBe(true); + }); + it("never upgrades a fabricated claim or conflicting claims into fresh proof", () => { + const doc = parseWhymark(input); + expect(evidenceViews(doc)).toEqual([]); + const note = doc.files[0].notes[0]; + note.verify.push({ method: "manual", status: "fail", raw: "manual => fail" }); + expect(hasPassingVerify(note)).toBe(false); + expect(readEvidence([{ version: 1, status: "pass" }])).toEqual([]); + }); + it("treats missing artifacts, timeout and mutation during verification as unavailable/stale", () => { + const cwd = repo(); const doc = parseWhymark(input); + doc.meta.checks = [{ cmd: 'node -e "setTimeout(() => {}, 10000)"', status: "unknown" }]; doc.files[0].notes[0].verify = []; + const results = verifyDocument(doc, { cwd, timeoutMs: 30, recordEvidence: true }); + expect(results[0].outcome).toBe("unrunnable"); + expect(localEvidence(doc, cwd)[0].state).toBe("unavailable"); + doc.meta.checks = [{ cmd: 'node -e "require(\'fs\').writeFileSync(\'file.ts\', \'changed\')"', status: "unknown" }]; + verifyDocument(doc, { cwd, recordEvidence: true }); + expect(localEvidence(doc, cwd).at(-1)?.state).toBe("stale"); + const fresh = parseWhymark(input); verifyDocument(fresh, { cwd, recordEvidence: true }); + const record = readEvidence(fresh.meta.extra.evidence)[0]; + expect(readFileSync(join(cwd, record.outputArtifact), "utf8")).toContain("checked"); + rmSync(join(cwd, record.outputArtifact)); + expect(localEvidence(fresh, cwd)[0].state).toBe("unavailable"); + }); + it("checks local locators without fetching URLs or executing claims", () => { + const cwd = repo(); const doc = parseWhymark(input + '\nsource: file:file.ts:99\nsource: file:../outside\nsource: url:http://127.0.0.1/private\n'); + expect(collectSourceDiagnostics(doc, cwd)).toHaveLength(2); + expect(sourceFingerprint(cwd)).toMatch(/^[a-f0-9]{64}$/); + }); +}); @note +1..81 kind=intent id=improvement-151 urgency: normal why: Recorded claims can outlive the code or tests they describe. Bind execution results to normalized review/source/output identities, retain contradictions and unavailable states, and check local citations without executing or fetching imported content. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/fixtures/multiple-notes.whymark added +43 newsha=a8e24cd @@ -0,0 +1,43 @@ +--- +whymark: 1 +title: Multiple comments and stable diff colors +scope: manual +summary: A fixture for overlapping annotations and horizontal scrolling. +--- + +@file example.ts modified +3 -3 +@@ -1,3 +1,3 @@ +-export const first = "old value with a deliberately long line for horizontal scrolling: abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz"; +-export const second = 2; +-export const third = 3; ++export const first = "new value with a deliberately long line for horizontal scrolling: abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz"; ++export const second = 20; ++export const third = 30; + +@note +1 kind=security risk=high confidence=0.8 id=security +urgency: urgent +why: Validate the new value at the boundary because downstream consumers assume it is trusted. +source: inference — fixture only +verify: none + +@note +1 kind=intent confidence=0.7 id=intent +urgency: normal +why: Keep the descriptive value so callers can inspect its purpose before using it. +verify: none + +@note +1..2 kind=test id=overlap +why: The same value has a second consumer, so the regression needs both call paths. +verify: none + +@note -1 kind=security id=old-security +why: The removed value was previously treated as trusted without validation. +verify: none + +@note +3 kind=note id=last +why: Preserve the separate third case so adjacent comment layout remains readable. +verify: none + +@file second.ts modified +1 -1 +@@ -1 +1 @@ +-export const x = 1; ++export const x = "long right-only overflow abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz"; @note +1..43 kind=intent id=improvement-152 urgency: normal why: Exercise observable behavior and failure boundaries rather than only matching implementation text: overlapping selection, reversible file decisions, evidence freshness, unsafe destinations and scanner cancellation must remain reliable. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/improvements.test.ts added +40 newsha=d1889c6 @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { parseWhymark } from "../src/lib/whymark/parse"; +import { serializeWhymark } from "../src/lib/whymark/serialize"; +import { buildRows } from "../src/lib/whymark/align"; +import { DEFAULT_PREFERENCES, parsePreferences } from "../src/lib/review-preferences"; + +const fixture = readFileSync("tests/fixtures/multiple-notes.whymark", "utf8"); + +describe("overlapping annotations and urgency", () => { + it("attaches every annotation to the exact side and range", () => { + const rows = buildRows(parseWhymark(fixture).files[0]); + expect(rows.find(row => row.newLine === 1)?.noteIds).toEqual(["security", "intent", "overlap"]); + expect(rows.find(row => row.oldLine === 1)?.noteIds).toEqual(["old-security"]); + }); + it("writes urgency as a v1-compatible field, including header input", () => { + const doc = parseWhymark(fixture.replace("kind=security risk=high", "kind=security urgency=urgent risk=high")); + expect(doc.files[0].notes[0].urgency).toBe("urgent"); + const text = serializeWhymark(doc); + expect(text).not.toContain("urgency="); + expect(parseWhymark(text).files[0].notes[0].urgency).toBe("urgent"); + }); + it("preserves unknown urgency and accepts legacy notes without it", () => { + const doc = parseWhymark(fixture.replace("urgency: urgent", "urgency: eventually")); + expect(doc.files[0].notes[0].urgency).toBeUndefined(); + expect(doc.diagnostics.some(item => item.code === "urgency-unknown")).toBe(true); + expect(serializeWhymark(doc)).toContain("urgency: eventually"); + expect(doc.files[0].notes[2].urgency).toBeUndefined(); + const header = parseWhymark(fixture.replace("kind=security risk=high", "kind=security urgency=future risk=high").replace("urgency: urgent\n", "")); + expect(serializeWhymark(header)).toContain("urgency: future"); + }); +}); + +describe("viewer preferences", () => { + it("ignores invalid storage and unrelated keys", () => { + expect(parsePreferences("invalid")).toEqual(DEFAULT_PREFERENCES); + expect(parsePreferences('{"version":2,"syncScroll":false}')).toEqual(DEFAULT_PREFERENCES); + expect(parsePreferences('{"version":1,"syncScroll":false,"code":"private","collapseNotes":"no"}')).toEqual({ ...DEFAULT_PREFERENCES, syncScroll: false }); + }); +}); @note +1..40 kind=intent id=improvement-153 urgency: normal why: Exercise observable behavior and failure boundaries rather than only matching implementation text: overlapping selection, reversible file decisions, evidence freshness, unsafe destinations and scanner cancellation must remain reliable. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/package.test.ts added +28 newsha=015e6f6 @@ -0,0 +1,28 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { expect, it } from "vitest"; + +it("installs the packed skill with only npm tarballs and no Git access", () => { + const temp = mkdtempSync(join(tmpdir(), "whymark-pack-")); + const run = (command: string, args: string[], cwd = resolve(".")) => { + const result = spawnSync(command, args, { cwd, encoding: "utf8", env: { ...process.env, npm_config_cache: join(temp, "cache"), npm_config_offline: "true", GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1" } }); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + return result.stdout; + }; + try { + const packed = JSON.parse(run("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", temp]))[0]; + expect(packed.files.map((f: { path: string }) => f.path)).toContain(".agents/skills/whymark/SKILL.md"); + const yaml = JSON.parse(run("npm", ["pack", "./node_modules/yaml", "--ignore-scripts", "--json", "--pack-destination", temp]))[0]; + const consumer = join(temp, "consumer"); mkdirSync(consumer); + writeFileSync(join(consumer, "package.json"), '{"private":true}'); + run("npm", ["install", "--offline", "--ignore-scripts", "--no-audit", "--no-fund", join(temp, packed.filename), join(temp, yaml.filename)], consumer); + const cli = join(consumer, "node_modules/whymark/bin/whymark.mjs"); + run(process.execPath, [cli, "skill", "install", "--agent", "codex", "--agent", "claude-code"], consumer); + const skill = join(consumer, ".agents/skills/whymark"); + expect(existsSync(join(skill, "references/whymark-v1.md"))).toBe(true); + expect(readFileSync(join(skill, "SKILL.md"), "utf8")).toContain("references/whymark-v1.md"); + expect(run(process.execPath, [cli, "skill", "install"], consumer)).toContain("unchanged"); + } finally { rmSync(temp, { recursive: true, force: true }); } +}, 30000); @note +1..28 kind=intent id=improvement-154 urgency: normal why: Exercise observable behavior and failure boundaries rather than only matching implementation text: overlapping selection, reversible file decisions, evidence freshness, unsafe destinations and scanner cancellation must remain reliable. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/quality-cli.test.ts added +56 newsha=a556d3b @@ -0,0 +1,56 @@ +import { execFileSync, spawn, spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { expect, it } from "vitest"; +const cli = resolve("dist/whymark.mjs"); +async function until(check: () => boolean, timeout = 12000) { + const start = Date.now(); + while (!check()) { + if (Date.now() - start > timeout) throw new Error("Timed out waiting for quality watcher"); + await new Promise(done => setTimeout(done, 30)); + } +} +it("cancels superseded scans and publishes only the latest source in watch mode", async () => { + const cwd = mkdtempSync(join(tmpdir(), "whymark-watch-")); + let child: ReturnType | undefined; + try { + execFileSync("git", ["init", "-q"], { cwd }); + mkdirSync(join(cwd, "reviews")); mkdirSync(join(cwd, ".artifacts")); + writeFileSync(join(cwd, "reviews/check.whymark"), "---\nwhymark: 1\ntitle: watch\n---\n"); + writeFileSync(join(cwd, "input.txt"), "old"); + writeFileSync(join(cwd, "check.cjs"), `const fs = require('node:fs'); const value = fs.readFileSync('input.txt', 'utf8'); fs.appendFileSync('.artifacts/started', value+'\\n'); setTimeout(() => console.log(value), 2500);`); + writeFileSync(join(cwd, "whymark.config.json"), JSON.stringify({ version: 1, checks: [{ id: "check", format: "check", command: "node check.cjs", versionCommand: "node --version" }] })); + child = spawn(process.execPath, [cli, "quality", "reviews/check.whymark", "--run", "--watch", "--write"], { cwd, stdio: ["ignore", "pipe", "pipe"] }); + let output = ""; let error = ""; + child.stdout!.on("data", data => { output += data; }); + child.stderr!.on("data", data => { error += data; }); + await until(() => existsSync(join(cwd, ".artifacts/started"))); + writeFileSync(join(cwd, "input.txt"), "new"); + await until(() => output.includes('"checks"')); + const report = JSON.parse(output); + expect(error).toBe(""); + expect(readFileSync(join(cwd, report.checks[0].outputArtifact), "utf8").trim()).toBe("new"); + expect(report.source).toBe(report.sourceAfter); + expect(readFileSync(join(cwd, "reviews/check.whymark"), "utf8")).toContain("quality:"); + } finally { + if (child && child.exitCode === null) { + const closed = new Promise(done => child!.once("close", done)); + child.kill("SIGTERM"); await closed; + } + rmSync(cwd, { recursive: true, force: true }); + } +}, 20000); + +it("imports ESLint findings without executing a review's embedded commands", () => { + const cwd = mkdtempSync(join(tmpdir(), "whymark-import-")); + try { + execFileSync("git", ["init", "-q"], { cwd }); mkdirSync(join(cwd, "reviews")); + writeFileSync(join(cwd, "reviews/check.whymark"), '---\nwhymark: 1\ntitle: import\nchecks:\n - cmd: touch unexpected-execution\n status: pass\n---\n'); + writeFileSync(join(cwd, "eslint.json"), JSON.stringify([{ filePath: "a.js", messages: [{ ruleId: "complexity", severity: 1, message: "Complexity is 20", line: 1 }] }])); + const result = spawnSync(process.execPath, [cli, "quality", "reviews/check.whymark", "--import", "eslint.json", "--write"], { cwd, encoding: "utf8" }); + expect(result.status).toBe(1); + expect(JSON.parse(result.stdout).provenance).toBe("imported"); + expect(existsSync(join(cwd, "unexpected-execution"))).toBe(false); + } finally { rmSync(cwd, { recursive: true, force: true }); } +}); @note +1..56 kind=intent id=improvement-155 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/quality.test.ts added +89 newsha=26a3326 @@ -0,0 +1,89 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { parseESLint, parseBoundedJson, readQuality } from "../src/lib/quality/import"; +import { applySuppressions, compareFindings, identifyFindings } from "../src/lib/quality/baseline"; +import { readConfig, runTool, scanQuality } from "../src/lib/quality/run"; +import { parseWhymark } from "../src/lib/whymark/parse"; +import type { QualityFinding, QualityReport } from "../src/lib/quality/types"; +const dirs: string[] = []; +const temp = () => { const dir = mkdtempSync(join(tmpdir(), "whymark-quality-")); dirs.push(dir); return dir; }; +afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); +const finding: QualityFinding = { id: "", tool: "eslint", rule: "complexity", severity: "warning", message: "Too complex", path: "a.ts", line: 2, endLine: 3, status: "uncompared" }; +function report(findings = [finding]): QualityReport { + return { version: 1, ran: new Date().toISOString(), source: "a".repeat(64), sourceAfter: "a".repeat(64), head: "b".repeat(40), clean: true, base: "b".repeat(40), reviewHash: "c".repeat(64), provenance: "local-run", comparison: "unavailable", checks: [{ id: "eslint", command: "eslint", version: "9", status: "pass", exitCode: 0, outputHash: "d".repeat(64), outputArtifact: "output.log", detail: "" }], findings }; +} +describe("quality import", () => { + it("normalizes locations and rejects malformed/unsafe/oversized reports", () => { + const json = JSON.stringify([{ filePath: "a.ts", messages: [{ ruleId: "complexity", severity: 1, message: "Too complex", line: 2 }] }]); + expect(parseESLint(json, name => name)[0]).toMatchObject({ path: "a.ts", severity: "warning", line: 2 }); + expect(() => parseESLint(json, () => "../outside")).toThrow(/escapes/); + expect(() => parseESLint('[{"filePath":"a.ts","messages":[{}]}]', name => name)).toThrow(/Malformed/); + expect(() => parseBoundedJson("x".repeat(8 * 1024 * 1024 + 1))).toThrow(/8 MB/); + expect(readQuality({ ...report(), findings: [{ ...finding, helpUrl: "javascript:alert(1)" }] })).toBeNull(); + expect(readQuality(report())).not.toBeNull(); + expect(readQuality({ ...report(), findings: [{ ...finding, suppression: null }] })).toBeNull(); + }); +}); +describe("baseline comparison", () => { + it("distinguishes existing, new and resolved findings across shifts and renames", () => { + const before = report([finding, { ...finding, rule: "removed" }]); + const after = report([{ ...finding, path: "b.ts", line: 20, endLine: 21 }, { ...finding, rule: "new" }]); + const result = compareFindings(after, before, new Map([["a.ts", "b.ts"]])); + expect(result.comparison).toBe("available"); + expect(result.findings.map(f => f.status)).toEqual(["existing", "new", "resolved"]); + }); + it("does not claim a clean baseline after failures or a different source revision", () => { + const before = report(); before.checks[0].status = "unavailable"; + expect(compareFindings(report(), before).comparison).toBe("unavailable"); + expect(compareFindings({ ...report(), base: "different" }, report()).findings[0].status).toBe("uncompared"); + expect(compareFindings(report(), { ...report(), clean: false }).comparison).toBe("unavailable"); + expect(compareFindings(report(), null).comparison).toBe("unavailable"); + }); + it("only applies suppressions with a reason and a future expiry", () => { + const findings = identifyFindings([finding]); + expect(applySuppressions(findings, [{ id: findings[0].id, reason: "legacy", expires: "2000-01-01" }])[0].suppression).toBeUndefined(); + expect(applySuppressions(findings, [{ id: findings[0].id, reason: "legacy", expires: "2100-01-01" }])[0].suppression?.reason).toBe("legacy"); + }); +}); +describe("configured checks", () => { + it("runs the installed ESLint against a real temporary project", async () => { + const cwd = temp(); execFileSync("git", ["init", "-q"], { cwd }); + writeFileSync(join(cwd, "a.js"), "const unused = 1;\n"); + writeFileSync(join(cwd, "eslint.config.mjs"), 'export default [{rules: {"no-unused-vars": "error"}}];'); + const executable = JSON.stringify(resolve("node_modules/eslint/bin/eslint.js")); + execFileSync("git", ["add", "a.js", "eslint.config.mjs"], { cwd }); + execFileSync("git", ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "-qm", "Baseline"], { cwd }); + const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(); + const doc = parseWhymark(`---\nwhymark: 1\ntitle: test\nbase: HEAD@${head.slice(0, 7)}\n---\n`); + const result = await scanQuality(doc, cwd, { version: 1, checks: [{ id: "eslint", format: "eslint", command: `node ${executable} . --format json`, versionCommand: `node ${executable} --version` }] }); + expect(result.checks[0].status).toBe("fail"); + expect(result.findings[0]).toMatchObject({ rule: "no-unused-vars", path: "a.js", status: "uncompared" }); + expect(result.source).toBe(result.sourceAfter); + expect(readQuality(result)).not.toBeNull(); + expect(result.clean).toBe(true); + expect(result.base).toBe(head); + writeFileSync(join(cwd, "a.js"), "\n\nconst unused = 1;\n"); + const changed = await scanQuality(doc, cwd, { version: 1, checks: [{ id: "eslint", format: "eslint", command: `node ${executable} . --format json`, versionCommand: `node ${executable} --version` }] }, { baseline: result }); + expect(changed.comparison).toBe("available"); + expect(changed.findings[0].status).toBe("existing"); + }); + it("reports missing tools, enforces bounded output and cancels a running process", async () => { + const cwd = temp(); + expect((await runTool("whymark-nonexistent-command", cwd)).unavailable).toBe(true); + const controller = new AbortController(); + const promise = runTool('node -e "setTimeout(() => {}, 20000)"', cwd, controller.signal); + setTimeout(() => controller.abort(), 30); + expect((await promise).unavailable).toBe(true); + expect((await runTool('node -e "process.stdout.write(\'x\'.repeat(9*1024*1024))"', cwd)).unavailable).toBe(true); + }); + it("rejects malformed configuration and never silently runs imported commands", () => { + const cwd = temp(); mkdirSync(join(cwd, "reviews")); + const path = join(cwd, "whymark.config.json"); writeFileSync(path, '{"version":1,"checks":[]}'); + expect(() => readConfig(path)).toThrow(/Invalid/); + const doc = parseWhymark('---\nwhymark: 1\ntitle: hostile\nchecks:\n - cmd: touch never-run\n status: pass\n---\n'); + expect(doc.meta.checks[0].cmd).toBe("touch never-run"); + }); +}); @note +1..89 kind=intent id=improvement-156 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file tests/skill-install.test.ts added +35 newsha=430bb3d @@ -0,0 +1,35 @@ +import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { installSkill } from "../src/lib/skill/install"; +const dirs: string[] = []; +const temp = () => { const dir = mkdtempSync(join(tmpdir(), "whymark-skill-")); dirs.push(dir); return dir; }; +afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); +const packageRoot = resolve("."); +describe("npm skill installer", () => { + it("previews, installs both agents, and is idempotent", () => { + const cwd = temp(); + const options = { packageRoot, cwd, agents: ["codex", "claude-code"] }; + expect(installSkill({ ...options, dryRun: true }).every(item => item.action === "create")).toBe(true); + expect(installSkill(options)).toHaveLength(4); + expect(installSkill(options).every(item => item.action === "unchanged")).toBe(true); + expect(readFileSync(join(cwd, ".agents/skills/whymark/SKILL.md"), "utf8")).toContain("references/whymark-v1.md"); + expect(readFileSync(join(cwd, ".claude/skills/whymark/references/whymark-v1.md"), "utf8")).toContain("Annotations"); + }); + it("preflights conflicts before writing another target", () => { + const cwd = temp(); + mkdirSync(join(cwd, ".claude/skills/whymark"), { recursive: true }); + writeFileSync(join(cwd, ".claude/skills/whymark/SKILL.md"), "custom"); + expect(() => installSkill({ packageRoot, cwd, agents: ["codex", "claude-code"] })).toThrow(/overwrite/); + expect(() => readFileSync(join(cwd, ".agents/skills/whymark/SKILL.md"))).toThrow(); + installSkill({ packageRoot, cwd, agents: ["claude-code"], force: true }); + }); + it("rejects symlink destinations even with force and scopes global installs", () => { + const cwd = temp(); const home = temp(); const outside = temp(); + symlinkSync(outside, join(cwd, ".agents")); + expect(() => installSkill({ packageRoot, cwd, force: true })).toThrow(/symlink/); + const installed = installSkill({ packageRoot, cwd, home, global: true }); + expect(installed.every(item => item.path.startsWith(realpathSync(home)))).toBe(true); + }); +}); @note +1..35 kind=intent id=improvement-157 urgency: normal why: Exercise observable behavior and failure boundaries rather than only matching implementation text: overlapping selection, reversible file decisions, evidence freshness, unsafe destinations and scanner cancellation must remain reliable. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s) @file whymark.config.json added +18 newsha=068ebe5 @@ -0,0 +1,18 @@ +{ + "version": 1, + "checks": [ + { + "id": "eslint", + "format": "eslint", + "command": "node node_modules/eslint/bin/eslint.js . --format json --rule 'complexity: [warn, 15]' --rule 'max-depth: [warn, 4]'", + "versionCommand": "node node_modules/eslint/bin/eslint.js --version" + }, + { + "id": "typecheck", + "format": "check", + "command": "npm run typecheck", + "versionCommand": "node node_modules/typescript/bin/tsc --version" + } + ], + "suppressions": [] +} @note +1..18 kind=intent id=improvement-158 urgency: normal why: Deterministic diagnostics provide inspectable feedback without model judgment. Configured tools run in project context; unavailable scans and dirty or incompatible baselines cannot become a clean comparison, and superseded scans must not replace newer results. source: file:specs/001-review-improvements/spec.md — approved requirements and acceptance boundaries verify: cmd `npm test` => pass (exit 0 in 5.1s)