spexcode 0.6.1 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -149
- package/bin/spex.mjs +4 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
- package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
- package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
- package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +115 -22
- package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +325 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
- package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
- package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
- package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
- package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
- package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
- package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
- package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
- package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
- package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
- package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
- package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
- package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
- package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
- package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
- package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
- package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
- package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
- package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +27 -7
- package/node_modules/@spexcode/spec-cli/package.json +47 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +22 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
- package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
- package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
- package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
- package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
- package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
- package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
- package/node_modules/@spexcode/spec-core/package.json +28 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
- package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
- package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
- package/node_modules/@spexcode/spec-eval/package.json +36 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
- package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
- package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
- package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
- package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
- package/node_modules/@spexcode/spec-forge/package.json +32 -0
- package/package.json +33 -19
- package/spec-cli/bin/spex.mjs +0 -69
- package/spec-cli/src/anchors.ts +0 -706
- package/spec-cli/src/attach.ts +0 -43
- package/spec-cli/src/claude-headless.ts +0 -321
- package/spec-cli/src/cli.ts +0 -1115
- package/spec-cli/src/client.ts +0 -315
- package/spec-cli/src/cockpit.ts +0 -43
- package/spec-cli/src/codex-runtime-generations.ts +0 -646
- package/spec-cli/src/commit-surgery.ts +0 -68
- package/spec-cli/src/contract-filter.ts +0 -202
- package/spec-cli/src/delivery-queue.ts +0 -107
- package/spec-cli/src/doctor.ts +0 -516
- package/spec-cli/src/file-write.ts +0 -22
- package/spec-cli/src/gateway-auth.ts +0 -181
- package/spec-cli/src/gateway-hub.ts +0 -356
- package/spec-cli/src/gateway.ts +0 -368
- package/spec-cli/src/git.ts +0 -2180
- package/spec-cli/src/graph.ts +0 -221
- package/spec-cli/src/graphCache.ts +0 -641
- package/spec-cli/src/graphDelta.ts +0 -77
- package/spec-cli/src/graphStream.ts +0 -1019
- package/spec-cli/src/harness-select.ts +0 -77
- package/spec-cli/src/harness.ts +0 -2747
- package/spec-cli/src/headless-controller.ts +0 -63
- package/spec-cli/src/help.ts +0 -480
- package/spec-cli/src/host-resources.ts +0 -707
- package/spec-cli/src/host.ts +0 -611
- package/spec-cli/src/identity-presets.d.ts +0 -13
- package/spec-cli/src/identity-presets.js +0 -128
- package/spec-cli/src/index.ts +0 -730
- package/spec-cli/src/init.ts +0 -258
- package/spec-cli/src/issues-cli.ts +0 -276
- package/spec-cli/src/issues.ts +0 -237
- package/spec-cli/src/layout.ts +0 -557
- package/spec-cli/src/lint.ts +0 -458
- package/spec-cli/src/localIssues.ts +0 -538
- package/spec-cli/src/materialize.ts +0 -412
- package/spec-cli/src/mentions.ts +0 -81
- package/spec-cli/src/opencode-headless.ts +0 -205
- package/spec-cli/src/pi-headless.ts +0 -153
- package/spec-cli/src/plugin-harness.ts +0 -157
- package/spec-cli/src/process-identity.ts +0 -207
- package/spec-cli/src/project-identity.ts +0 -73
- package/spec-cli/src/project-store.ts +0 -17
- package/spec-cli/src/pty-bridge.ts +0 -396
- package/spec-cli/src/pty-helper.mjs +0 -96
- package/spec-cli/src/pty-native-helper.mjs +0 -22
- package/spec-cli/src/ranker.ts +0 -191
- package/spec-cli/src/reaper.ts +0 -125
- package/spec-cli/src/resilience.ts +0 -41
- package/spec-cli/src/reviewSnapshot.ts +0 -28
- package/spec-cli/src/reviews.ts +0 -405
- package/spec-cli/src/runtime-guard.ts +0 -35
- package/spec-cli/src/runtime-ownership.ts +0 -88
- package/spec-cli/src/search.bench.mjs +0 -80
- package/spec-cli/src/search.ts +0 -63
- package/spec-cli/src/session-cursors.ts +0 -91
- package/spec-cli/src/session-follow.ts +0 -182
- package/spec-cli/src/session-timeline.ts +0 -98
- package/spec-cli/src/sessions.ts +0 -3403
- package/spec-cli/src/sh.ts +0 -3
- package/spec-cli/src/slash-commands.ts +0 -318
- package/spec-cli/src/source-files.ts +0 -104
- package/spec-cli/src/specs.ts +0 -498
- package/spec-cli/src/supervise.ts +0 -245
- package/spec-cli/src/tree.ts +0 -118
- package/spec-cli/src/tsx-bin.ts +0 -18
- package/spec-cli/src/uninstall.ts +0 -171
- package/spec-cli/src/uploads.ts +0 -253
- package/spec-cli/src/worktree-sources.ts +0 -42
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
- package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -28
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +0 -23
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -32
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -9
- package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -15
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -18
- package/spec-dashboard/dist/assets/App-F9uaAVcH.js +0 -2
- package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
- package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-CuKLFhH3.js +0 -1
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
- package/spec-dashboard/dist/assets/Modal-CQgYymmr.js +0 -1
- package/spec-dashboard/dist/assets/PageScroll-hT7UTLvD.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-CtXxakF9.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
- package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
- package/spec-dashboard/dist/assets/SessionWindow-CixDi4PI.js +0 -1
- package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
- package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
- package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
- package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
- package/spec-dashboard/dist/assets/index-DrVao0Ep.js +0 -41
- package/spec-dashboard/dist/assets/launch-BBH02b1v.js +0 -276
- package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
- package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
- package/spec-dashboard/dist/index.html +0 -29
- package/spec-dashboard/src/reviewFilters.js +0 -322
- package/spec-dashboard/src/reviewQuery.js +0 -174
- package/spec-dashboard/src/session.js +0 -199
- package/spec-eval/src/cache.ts +0 -57
- package/spec-eval/src/cli.ts +0 -889
- package/spec-eval/src/evaltab.ts +0 -262
- package/spec-eval/src/filing.ts +0 -39
- package/spec-eval/src/freshness.ts +0 -557
- package/spec-eval/src/humanok.ts +0 -28
- package/spec-eval/src/scenariofresh.ts +0 -244
- package/spec-eval/src/scenarios.ts +0 -672
- package/spec-eval/src/sessioneval.ts +0 -2226
- package/spec-eval/src/sidecar.ts +0 -93
- package/spec-eval/src/timeline.ts +0 -54
- package/spec-forge/src/__fixtures__/github-forge.json +0 -63
- package/spec-forge/src/cache.ts +0 -66
- package/spec-forge/src/cli.ts +0 -99
- package/spec-forge/src/drivers/github.ts +0 -132
- package/spec-forge/src/drivers/gitlab.ts +0 -153
- package/spec-forge/src/drivers.ts +0 -73
- package/spec-forge/src/links.ts +0 -72
- package/spec-forge/src/needs-eval.ts +0 -29
- package/spec-forge/src/port.ts +0 -36
- package/spec-forge/src/resident.ts +0 -46
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { batchRevisionOids, gitTry, headSha, gitObjectInterpretation, currentGitBuildAbortSignal, gitAbortError, ancestorsOf, inAncestors, commitReachable, pathEvents, primeAncestorClosures, unionTopology, eventsSince } from '@spexcode/spec-core';
|
|
5
|
+
import { anchorHitExists, extOf, extractorFor, extractors, resolveSelectors } from '@spexcode/spec-core';
|
|
6
|
+
import { scenarioCodeAxis, scenarioHash } from './scenarios.js';
|
|
7
|
+
import { scenarioChangeCommits, scenarioBlocksAt, primeScenarioBlocks } from './scenariofresh.js';
|
|
8
|
+
const rootScopes = new Map();
|
|
9
|
+
// Roots come and go (a closed worktree never asks again), so cap how many stay warm — the same bounded-slot
|
|
10
|
+
// guard the index caches use, and the only bound needed once per-root cardinality is the corpus, not history.
|
|
11
|
+
const ROOT_SLOTS = Math.max(4, Number(process.env.SPEXCODE_FRESHNESS_ROOT_SLOTS || 64));
|
|
12
|
+
function scopeFor(rootKey, head, currentOid) {
|
|
13
|
+
const current = rootScopes.get(rootKey);
|
|
14
|
+
if (current?.head === head && (current.currentOid === currentOid || currentOid === null || current.currentOid === null)) {
|
|
15
|
+
if (currentOid !== null)
|
|
16
|
+
current.currentOid = currentOid;
|
|
17
|
+
rootScopes.delete(rootKey);
|
|
18
|
+
rootScopes.set(rootKey, current);
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
const scope = { head, currentOid, anchors: new Map(), topology: null, topologyRoster: new Set(), topologyFlight: null, blockDemands: new Set(), anchorVerdicts: new Map() };
|
|
22
|
+
rootScopes.set(rootKey, scope);
|
|
23
|
+
while (rootScopes.size > ROOT_SLOTS) {
|
|
24
|
+
const oldest = rootScopes.keys().next().value;
|
|
25
|
+
if (oldest === undefined || oldest === rootKey)
|
|
26
|
+
break;
|
|
27
|
+
rootScopes.delete(oldest);
|
|
28
|
+
}
|
|
29
|
+
return scope;
|
|
30
|
+
}
|
|
31
|
+
// a read only ever sees the root's CURRENT head — a probe pinned to a superseded head can't testify
|
|
32
|
+
function currentScope(rootKey, head) {
|
|
33
|
+
const scope = rootScopes.get(rootKey);
|
|
34
|
+
return scope?.head === head ? scope : undefined;
|
|
35
|
+
}
|
|
36
|
+
// the cardinality invariant, made observable: how many anchor entries are resident at a root's CURRENT head
|
|
37
|
+
// (every root when none is named). It must track the corpus's anchors, never how many times the board has
|
|
38
|
+
// been rebuilt — one retained generation, not one per rebuild.
|
|
39
|
+
export function freshnessCacheSize(root) {
|
|
40
|
+
if (root !== undefined)
|
|
41
|
+
return rootScopes.get(resolve(root))?.anchors.size ?? 0;
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const scope of rootScopes.values())
|
|
44
|
+
total += scope.anchors.size;
|
|
45
|
+
return total;
|
|
46
|
+
}
|
|
47
|
+
// The same invariant for the OTHER probe in this scope. It needs its own counter rather than a bigger number
|
|
48
|
+
// from the one above: the two hold different things (content anchor entries vs selector-anchor verdicts), and
|
|
49
|
+
// a counter that cannot see a case is exactly how "one retained generation, not one per rebuild" came to be
|
|
50
|
+
// declared, observable, and still violated ([[selector-anchor-scope]]).
|
|
51
|
+
export function anchorVerdictCacheSize(root) {
|
|
52
|
+
if (root !== undefined)
|
|
53
|
+
return rootScopes.get(resolve(root))?.anchorVerdicts.size ?? 0;
|
|
54
|
+
let total = 0;
|
|
55
|
+
for (const scope of rootScopes.values())
|
|
56
|
+
total += scope.anchorVerdicts.size;
|
|
57
|
+
return total;
|
|
58
|
+
}
|
|
59
|
+
function touchAnchor(scope, sha, imageOid) {
|
|
60
|
+
const hit = scope.anchors.get(sha);
|
|
61
|
+
if (hit && imageOid === undefined)
|
|
62
|
+
return hit;
|
|
63
|
+
if (hit && hit.imageOid == null && imageOid !== undefined) {
|
|
64
|
+
hit.imageOid = imageOid;
|
|
65
|
+
hit.gone = imageOid === null;
|
|
66
|
+
return hit;
|
|
67
|
+
}
|
|
68
|
+
if (hit && hit.imageOid === imageOid)
|
|
69
|
+
return hit;
|
|
70
|
+
const entry = { imageOid, verdicts: new Map(), gone: imageOid === null, pending: new Set(), flight: null };
|
|
71
|
+
scope.anchors.set(sha, entry);
|
|
72
|
+
return entry;
|
|
73
|
+
}
|
|
74
|
+
const heavyDiffScopes = new Map();
|
|
75
|
+
// One tree comparison at a time is enough for a production repo's pack/index memory. The graph-wide git
|
|
76
|
+
// permits still bound every child; this narrower domain scheduler keeps these immutable-tree
|
|
77
|
+
// comparisons serial per (repo, HEAD), without teaching the git transport what a content fallback is.
|
|
78
|
+
function acquireHeavyDiff(scopeKey, signal) {
|
|
79
|
+
if (signal?.aborted)
|
|
80
|
+
return Promise.reject(gitAbortError());
|
|
81
|
+
let scope = heavyDiffScopes.get(scopeKey);
|
|
82
|
+
if (!scope) {
|
|
83
|
+
scope = { active: false, waiting: [] };
|
|
84
|
+
heavyDiffScopes.set(scopeKey, scope);
|
|
85
|
+
}
|
|
86
|
+
const releaseFor = () => {
|
|
87
|
+
let released = false;
|
|
88
|
+
return () => {
|
|
89
|
+
if (released)
|
|
90
|
+
return;
|
|
91
|
+
released = true;
|
|
92
|
+
scope.active = false;
|
|
93
|
+
while (scope.waiting.length) {
|
|
94
|
+
const waiter = scope.waiting.shift();
|
|
95
|
+
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
96
|
+
if (waiter.signal?.aborted) {
|
|
97
|
+
waiter.reject(gitAbortError());
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
scope.active = true;
|
|
101
|
+
waiter.resolve(releaseFor());
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (heavyDiffScopes.get(scopeKey) === scope)
|
|
105
|
+
heavyDiffScopes.delete(scopeKey);
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
if (!scope.active) {
|
|
109
|
+
scope.active = true;
|
|
110
|
+
return Promise.resolve(releaseFor());
|
|
111
|
+
}
|
|
112
|
+
return new Promise((resolvePermit, reject) => {
|
|
113
|
+
const waiter = {
|
|
114
|
+
signal,
|
|
115
|
+
resolve: resolvePermit,
|
|
116
|
+
reject,
|
|
117
|
+
onAbort: () => {
|
|
118
|
+
const index = scope.waiting.indexOf(waiter);
|
|
119
|
+
if (index < 0)
|
|
120
|
+
return;
|
|
121
|
+
scope.waiting.splice(index, 1);
|
|
122
|
+
signal?.removeEventListener('abort', waiter.onAbort);
|
|
123
|
+
reject(gitAbortError());
|
|
124
|
+
if (!scope.active && scope.waiting.length === 0 && heavyDiffScopes.get(scopeKey) === scope)
|
|
125
|
+
heavyDiffScopes.delete(scopeKey);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
scope.waiting.push(waiter);
|
|
129
|
+
signal?.addEventListener('abort', waiter.onAbort, { once: true });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// @@@ one bounded batch per planned population, only the paths actually asked about - the tree comparison is pathspec-scoped
|
|
133
|
+
// (`:(literal)` so a path holding a glob char, a space or a leading colon is matched verbatim, `-z` so the
|
|
134
|
+
// answer needs no unquoting), and the answer retained is one boolean per REQUESTED path. Concurrent primes
|
|
135
|
+
// on the same anchor therefore union their paths BEFORE the child starts: a caller records what it needs in
|
|
136
|
+
// `pending`, then either starts the batch that drains pending or joins the running one and re-checks after —
|
|
137
|
+
// so a path requested mid-flight rides the NEXT batch instead of racing this one, and a settled path is
|
|
138
|
+
// never asked again. Every bounded chunk stays serial through the same scope permit.
|
|
139
|
+
const CONTENT_BATCH_RECORD_LIMIT = 8_192;
|
|
140
|
+
const CONTENT_BATCH_ARGV_BYTE_LIMIT = 16 * 1_024;
|
|
141
|
+
const CONTENT_BATCH_MARKER = '\x1espex-content:';
|
|
142
|
+
function contentBatchArgs(root, paths) {
|
|
143
|
+
return [
|
|
144
|
+
'-C', root, '-c', 'core.quotePath=false', 'diff-tree', '--stdin', '--always', '-r', '--name-status', '-z',
|
|
145
|
+
'--no-renames', '--no-color', '--no-ext-diff', '--no-textconv', `--format=${CONTENT_BATCH_MARKER}%H`,
|
|
146
|
+
'--', ...paths.map((path) => `:(literal)${path}`),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
function argvBytes(args) {
|
|
150
|
+
return args.reduce((total, arg) => total + Buffer.byteLength(arg) + 1, 0);
|
|
151
|
+
}
|
|
152
|
+
function contentBatchChunks(root, rows) {
|
|
153
|
+
const chunks = [];
|
|
154
|
+
let byRow = new Map();
|
|
155
|
+
let paths = new Set();
|
|
156
|
+
const flush = () => {
|
|
157
|
+
if (byRow.size)
|
|
158
|
+
chunks.push([...byRow.values()]);
|
|
159
|
+
byRow = new Map();
|
|
160
|
+
paths = new Set();
|
|
161
|
+
};
|
|
162
|
+
for (const row of rows) {
|
|
163
|
+
for (const path of row.requested) {
|
|
164
|
+
for (;;) {
|
|
165
|
+
const nextPaths = paths.has(path) ? paths : new Set(paths).add(path);
|
|
166
|
+
const nextRows = byRow.size + (byRow.has(row) ? 0 : 1);
|
|
167
|
+
const withinRecords = nextRows * nextPaths.size <= CONTENT_BATCH_RECORD_LIMIT;
|
|
168
|
+
const withinArgv = argvBytes(contentBatchArgs(root, [...nextPaths])) <= CONTENT_BATCH_ARGV_BYTE_LIMIT;
|
|
169
|
+
if (withinRecords && withinArgv)
|
|
170
|
+
break;
|
|
171
|
+
if (!byRow.size)
|
|
172
|
+
throw new Error(`git content diff path exceeds the ${CONTENT_BATCH_ARGV_BYTE_LIMIT}-byte argv bound: ${path}`);
|
|
173
|
+
flush();
|
|
174
|
+
}
|
|
175
|
+
let slice = byRow.get(row);
|
|
176
|
+
if (!slice) {
|
|
177
|
+
slice = { ...row, requested: [] };
|
|
178
|
+
byRow.set(row, slice);
|
|
179
|
+
}
|
|
180
|
+
slice.requested.push(path);
|
|
181
|
+
paths.add(path);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
flush();
|
|
185
|
+
return chunks;
|
|
186
|
+
}
|
|
187
|
+
function parseContentBatch(out, rows) {
|
|
188
|
+
let rowIndex = -1;
|
|
189
|
+
let status = null;
|
|
190
|
+
const changed = rows.map(() => new Set());
|
|
191
|
+
for (const raw of out.split('\0')) {
|
|
192
|
+
if (!raw)
|
|
193
|
+
continue;
|
|
194
|
+
const token = raw.startsWith('\n') ? raw.slice(1) : raw;
|
|
195
|
+
if (status !== null) {
|
|
196
|
+
if (rowIndex < 0 || rowIndex >= rows.length)
|
|
197
|
+
throw new Error('git content diff batch emitted a path before its pair marker');
|
|
198
|
+
changed[rowIndex].add(token);
|
|
199
|
+
status = null;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (token.startsWith(CONTENT_BATCH_MARKER)) {
|
|
203
|
+
rowIndex++;
|
|
204
|
+
if (rowIndex >= rows.length)
|
|
205
|
+
throw new Error('git content diff batch emitted too many pair markers');
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!/^[A-Z](?:\d+)?$/.test(token))
|
|
209
|
+
throw new Error(`git content diff batch emitted malformed status '${token}'`);
|
|
210
|
+
status = token;
|
|
211
|
+
}
|
|
212
|
+
if (status !== null)
|
|
213
|
+
throw new Error(`git content diff batch ended after status '${status}'`);
|
|
214
|
+
if (rowIndex + 1 !== rows.length)
|
|
215
|
+
throw new Error(`git content diff batch emitted ${rowIndex + 1} pair markers for ${rows.length} pairs`);
|
|
216
|
+
return changed;
|
|
217
|
+
}
|
|
218
|
+
async function resolvedContentImages(root, raws, replacements) {
|
|
219
|
+
const targets = raws.map((raw) => {
|
|
220
|
+
let target = raw;
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
while (replacements.has(target)) {
|
|
223
|
+
if (seen.has(target))
|
|
224
|
+
throw new Error(`refs/replace cycle while resolving content image '${raw}'`);
|
|
225
|
+
seen.add(target);
|
|
226
|
+
target = replacements.get(target);
|
|
227
|
+
}
|
|
228
|
+
return target;
|
|
229
|
+
});
|
|
230
|
+
// One no-replace child proves both the names callers supplied and the exact final replacement targets.
|
|
231
|
+
// A target cannot make a nonexistent raw id look available, and a missing replacement target is loud.
|
|
232
|
+
const queries = [...new Set([...raws, ...targets])];
|
|
233
|
+
const answers = await batchRevisionOids(root, queries, { replaceObjects: false });
|
|
234
|
+
const checked = new Map(queries.map((query, index) => [query, answers[index]]));
|
|
235
|
+
return raws.map((raw, index) => {
|
|
236
|
+
if (!checked.get(raw))
|
|
237
|
+
return null;
|
|
238
|
+
const target = checked.get(targets[index]);
|
|
239
|
+
if (!target)
|
|
240
|
+
throw new Error(`replacement target '${targets[index]}' for content image '${raw}' is unreadable`);
|
|
241
|
+
return target;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async function runContentBatch(root, currentOid, rows) {
|
|
245
|
+
const settled = [];
|
|
246
|
+
for (const chunk of contentBatchChunks(root, rows)) {
|
|
247
|
+
const paths = [...new Set(chunk.flatMap((row) => row.requested))].sort();
|
|
248
|
+
const result = await gitTry(contentBatchArgs(root, paths), {
|
|
249
|
+
input: chunk.map((row) => `${row.oid} ${currentOid}`).join('\n') + '\n',
|
|
250
|
+
// batchRevisionOids already froze replacements/grafts into exact object ids. Do not reinterpret those
|
|
251
|
+
// ids if refs/replace moves between the resolution child and this content child.
|
|
252
|
+
extraEnv: { GIT_NO_REPLACE_OBJECTS: '1' },
|
|
253
|
+
});
|
|
254
|
+
if (!result.ok)
|
|
255
|
+
throw new Error(`git content diff batch failed (${result.failure ?? 'unknown'}): ${result.stderr.trim() || 'unknown git error'}`);
|
|
256
|
+
parseContentBatch(result.stdout, chunk).forEach((changed, index) => settled.push({ row: chunk[index], changed }));
|
|
257
|
+
}
|
|
258
|
+
// Publish only after every chunk succeeds. A late transport failure leaves the entire planned population
|
|
259
|
+
// retryable instead of turning its early chunks into an accidental partial freshness answer.
|
|
260
|
+
for (const { row, changed } of settled)
|
|
261
|
+
for (const path of row.requested)
|
|
262
|
+
row.entry.verdicts.set(path, changed.has(path));
|
|
263
|
+
}
|
|
264
|
+
function startPluralContentBatch(root, rootKey, scope, rows) {
|
|
265
|
+
const run = async () => {
|
|
266
|
+
const release = await acquireHeavyDiff(`${rootKey}\x1f${scope.head}`, currentGitBuildAbortSignal());
|
|
267
|
+
const batch = rows.map((row) => ({ ...row, oid: row.entry.imageOid, requested: [...row.entry.pending] }))
|
|
268
|
+
.filter((row) => row.requested.length);
|
|
269
|
+
for (const row of batch)
|
|
270
|
+
row.entry.pending.clear();
|
|
271
|
+
try {
|
|
272
|
+
await runContentBatch(root, scope.currentOid, batch);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
release();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
let flight;
|
|
279
|
+
flight = run().finally(() => {
|
|
280
|
+
for (const row of rows)
|
|
281
|
+
if (row.entry.flight === flight)
|
|
282
|
+
row.entry.flight = null;
|
|
283
|
+
});
|
|
284
|
+
for (const row of rows)
|
|
285
|
+
row.entry.flight = flight;
|
|
286
|
+
return flight;
|
|
287
|
+
}
|
|
288
|
+
// @@@ the drift COUNT is a reachability question, so ONE walk answers the whole roster - counting it as
|
|
289
|
+
// `rev-list --count <anchor>..<HEAD> -- <path>` names an OFF-HISTORY range, so Git can never cut the walk
|
|
290
|
+
// short: every (anchor, path) pair traverses the entire history, and the pairs multiply. Measured cold on a
|
|
291
|
+
// 437-anchor deployment scope: 1748 of the read's 1750 git children were those counts. What HEAD's index
|
|
292
|
+
// genuinely lacks is only the ANCHORS' own ancestry, and one `rev-list --parents` walk over the whole roster
|
|
293
|
+
// carries it; the count is then the same in-memory rule `eventsSince` applies on the in-history side. The
|
|
294
|
+
// walk is a SEPARATE structure from the shared drift index on purpose: grafting off-history tips into that
|
|
295
|
+
// index would make `ancestorsOf` stop answering undefined for them and silently switch the freshness
|
|
296
|
+
// DECISION from this content probe to ancestry. A rejected walk caches nothing and every joiner sees the
|
|
297
|
+
// same failure, matching the anchor batch above.
|
|
298
|
+
async function primeAnchorTopology(root, scope, oids) {
|
|
299
|
+
for (;;) {
|
|
300
|
+
const missing = oids.filter((oid) => !scope.topologyRoster.has(oid));
|
|
301
|
+
if (!missing.length)
|
|
302
|
+
return;
|
|
303
|
+
// an anchor that arrives mid-walk rides the NEXT one instead of racing this one, exactly as a
|
|
304
|
+
// mid-flight path rides the next content batch.
|
|
305
|
+
if (scope.topologyFlight) {
|
|
306
|
+
await scope.topologyFlight;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const roster = [...new Set([...scope.topologyRoster, ...missing])];
|
|
310
|
+
const run = async () => {
|
|
311
|
+
const reach = await unionTopology(root, roster);
|
|
312
|
+
// one child-before-parent pass for the whole roster; the bytes are identical to asking one by one.
|
|
313
|
+
primeAncestorClosures(reach, roster);
|
|
314
|
+
scope.topology = reach;
|
|
315
|
+
scope.topologyRoster = new Set(roster);
|
|
316
|
+
};
|
|
317
|
+
let flight;
|
|
318
|
+
flight = run().finally(() => { if (scope.topologyFlight === flight)
|
|
319
|
+
scope.topologyFlight = null; });
|
|
320
|
+
scope.topologyFlight = flight;
|
|
321
|
+
await flight;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export function contentProbeFor(root) {
|
|
325
|
+
const rootKey = resolve(root);
|
|
326
|
+
let head;
|
|
327
|
+
let activeImageKey;
|
|
328
|
+
const headOf = () => (head ??= headSha(root));
|
|
329
|
+
const imageOf = () => {
|
|
330
|
+
const rawHead = headOf();
|
|
331
|
+
const interpretation = gitObjectInterpretation(root);
|
|
332
|
+
return { rawHead, key: `${rawHead}\x1f${interpretation.identity}`, interpretation };
|
|
333
|
+
};
|
|
334
|
+
const primeMany = async (demands) => {
|
|
335
|
+
if (!demands.length)
|
|
336
|
+
return;
|
|
337
|
+
const planned = new Map();
|
|
338
|
+
for (const demand of demands) {
|
|
339
|
+
const row = planned.get(demand.anchorSha) ?? { paths: new Set(), codePaths: new Set(), evalPaths: new Set() };
|
|
340
|
+
for (const path of demand.paths) {
|
|
341
|
+
if (path) {
|
|
342
|
+
row.paths.add(path);
|
|
343
|
+
row.codePaths.add(path);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (demand.evalPath) {
|
|
347
|
+
row.paths.add(demand.evalPath);
|
|
348
|
+
row.evalPaths.add(demand.evalPath);
|
|
349
|
+
}
|
|
350
|
+
planned.set(demand.anchorSha, row);
|
|
351
|
+
}
|
|
352
|
+
let scope;
|
|
353
|
+
for (;;) {
|
|
354
|
+
const image = imageOf();
|
|
355
|
+
activeImageKey = image.key;
|
|
356
|
+
const current = scopeFor(rootKey, image.key, null);
|
|
357
|
+
for (const [sha, row] of planned) {
|
|
358
|
+
const entry = touchAnchor(current, sha, undefined);
|
|
359
|
+
for (const path of row.paths)
|
|
360
|
+
if (!entry.verdicts.has(path))
|
|
361
|
+
entry.pending.add(path);
|
|
362
|
+
}
|
|
363
|
+
const needsResolution = current.currentOid === null || [...planned].some(([sha]) => {
|
|
364
|
+
const entry = current.anchors.get(sha);
|
|
365
|
+
return entry.imageOid === undefined || entry.gone;
|
|
366
|
+
});
|
|
367
|
+
if (!needsResolution) {
|
|
368
|
+
scope = current;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
// Resolve HEAD and every anchor in ONE child, then require the interpretation inputs to remain stable.
|
|
372
|
+
// The returned object ids become the only images downstream commands may execute against.
|
|
373
|
+
const shas = [...planned.keys()];
|
|
374
|
+
const resolved = await resolvedContentImages(root, [image.rawHead, ...shas], image.interpretation.replacements);
|
|
375
|
+
if (imageOf().key !== image.key)
|
|
376
|
+
continue;
|
|
377
|
+
const currentOid = resolved[0];
|
|
378
|
+
if (!currentOid)
|
|
379
|
+
throw new Error(`git content diff current image '${image.rawHead}' is unreadable`);
|
|
380
|
+
scope = scopeFor(rootKey, image.key, currentOid);
|
|
381
|
+
shas.forEach((sha, index) => touchAnchor(scope, sha, resolved[index + 1]));
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
const grouped = new Map();
|
|
385
|
+
for (const [sha, row] of planned) {
|
|
386
|
+
const entry = scope.anchors.get(sha);
|
|
387
|
+
if (!entry)
|
|
388
|
+
throw new Error(`git content diff did not resolve planned anchor '${sha}'`);
|
|
389
|
+
grouped.set(sha, { entry, ...row });
|
|
390
|
+
}
|
|
391
|
+
for (;;) {
|
|
392
|
+
const waiting = new Set();
|
|
393
|
+
const ready = [];
|
|
394
|
+
for (const [sha, row] of grouped) {
|
|
395
|
+
if (row.entry.gone)
|
|
396
|
+
continue;
|
|
397
|
+
const missing = [...row.paths].filter((path) => !row.entry.verdicts.has(path));
|
|
398
|
+
if (!missing.length)
|
|
399
|
+
continue;
|
|
400
|
+
for (const path of missing)
|
|
401
|
+
row.entry.pending.add(path);
|
|
402
|
+
if (row.entry.flight)
|
|
403
|
+
waiting.add(row.entry.flight);
|
|
404
|
+
else
|
|
405
|
+
ready.push({ sha, entry: row.entry });
|
|
406
|
+
}
|
|
407
|
+
if (waiting.size) {
|
|
408
|
+
await Promise.all(waiting);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (!ready.length)
|
|
412
|
+
break;
|
|
413
|
+
await startPluralContentBatch(root, rootKey, scope, ready);
|
|
414
|
+
}
|
|
415
|
+
// only anchors whose governed content actually differs need their past: those are the readings that
|
|
416
|
+
// will render a drift count, and a byte-identical population walks nothing at all.
|
|
417
|
+
const drifted = [...grouped.values()]
|
|
418
|
+
.filter((row) => !row.entry.gone && [...row.codePaths].some((path) => row.entry.verdicts.get(path) === true))
|
|
419
|
+
.map((row) => row.entry.imageOid);
|
|
420
|
+
if (drifted.length)
|
|
421
|
+
await primeAnchorTopology(root, scope, drifted);
|
|
422
|
+
for (const [sha, row] of grouped) {
|
|
423
|
+
if (row.entry.gone)
|
|
424
|
+
continue;
|
|
425
|
+
for (const evalPath of row.evalPaths)
|
|
426
|
+
if (row.entry.verdicts.get(evalPath) === true) {
|
|
427
|
+
scope.blockDemands.add(`${row.entry.imageOid}\x1f${evalPath}`);
|
|
428
|
+
scope.blockDemands.add(`${scope.currentOid}\x1f${evalPath}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
async prime(sha, paths, evalPath) {
|
|
434
|
+
await primeMany([{ anchorSha: sha, paths, evalPath }]);
|
|
435
|
+
},
|
|
436
|
+
primeMany,
|
|
437
|
+
async primeBlocks() {
|
|
438
|
+
const image = imageOf();
|
|
439
|
+
activeImageKey = image.key;
|
|
440
|
+
const scope = currentScope(rootKey, image.key);
|
|
441
|
+
if (!scope?.blockDemands.size)
|
|
442
|
+
return;
|
|
443
|
+
const demands = [...scope.blockDemands].map((k) => {
|
|
444
|
+
const cut = k.indexOf('\x1f');
|
|
445
|
+
return { rev: k.slice(0, cut), path: k.slice(cut + 1) };
|
|
446
|
+
});
|
|
447
|
+
scope.blockDemands.clear();
|
|
448
|
+
await primeScenarioBlocks(root, demands);
|
|
449
|
+
},
|
|
450
|
+
changed(sha, path) {
|
|
451
|
+
// The async prime owns all I/O. An unprimed path is 'can't testify', never a fresh sync diff: a miss
|
|
452
|
+
// stays conservative and no abort/transient failure can turn into an unbounded synchronous fallback.
|
|
453
|
+
const entry = activeImageKey ? currentScope(rootKey, activeImageKey)?.anchors.get(sha) : undefined;
|
|
454
|
+
if (!entry || entry.gone)
|
|
455
|
+
return null;
|
|
456
|
+
return entry.verdicts.get(path) ?? null;
|
|
457
|
+
},
|
|
458
|
+
canTestify(sha) {
|
|
459
|
+
// a settled verdict — of either polarity — is the proof the anchor's tree was readable
|
|
460
|
+
const entry = activeImageKey ? currentScope(rootKey, activeImageKey)?.anchors.get(sha) : undefined;
|
|
461
|
+
return !!entry && !entry.gone && entry.verdicts.size > 0;
|
|
462
|
+
},
|
|
463
|
+
scenarioDiffers(sha, evalPath, scenario) {
|
|
464
|
+
const scope = activeImageKey ? currentScope(rootKey, activeImageKey) : undefined;
|
|
465
|
+
const entry = scope?.anchors.get(sha);
|
|
466
|
+
const a = entry?.imageOid ? scenarioBlocksAt(root, entry.imageOid, evalPath) : null;
|
|
467
|
+
if (!a)
|
|
468
|
+
return true; // eval.md unreadable at the anchor (renamed/absent) → can't prove → stale
|
|
469
|
+
return a.get(scenario) !== scenarioBlocksAt(root, scope.currentOid, evalPath)?.get(scenario);
|
|
470
|
+
},
|
|
471
|
+
inAnchorPast(sha, commit) {
|
|
472
|
+
const scope = activeImageKey ? currentScope(rootKey, activeImageKey) : undefined;
|
|
473
|
+
const oid = scope?.anchors.get(sha)?.imageOid;
|
|
474
|
+
if (!scope?.topology || !oid)
|
|
475
|
+
return false;
|
|
476
|
+
const bits = ancestorsOf(scope.topology, oid);
|
|
477
|
+
return !!bits && inAncestors(scope.topology, bits, commit);
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
// a verdict answers ONE selector set, so the set is part of its identity: several scenarios anchoring
|
|
482
|
+
// DIFFERENT units of one shared file is the whole point of narrowing, and keying only by (sha, path) would
|
|
483
|
+
// hand the first one's answer to all the others — silently, and in the fresh direction.
|
|
484
|
+
const anchorKey = (sinceSha, path, selectors) => `${sinceSha}\x1f${path}\x1f${[...selectors].sort().join('\x1e')}`;
|
|
485
|
+
// @@@ one parse per CONTENT, not per selector entry - a node's `code:` entries are asked one at a time, and
|
|
486
|
+
// the same working-tree file backs many of them, so the unmemoized read parsed 46 distinct files 922 times
|
|
487
|
+
// per build (40.4 MB through the TypeScript parser) and did it again on every rebuild. `extract` is a pure
|
|
488
|
+
// function of (text, path, extractor), so its result is reusable exactly as far as the CONTENT is unchanged.
|
|
489
|
+
// The key is a content digest and never mtime/size: this gate decides whether a reading may testify, so a
|
|
490
|
+
// stale unit list would let a dead selector read as alive — the precise failure the comment below warns
|
|
491
|
+
// about. Digesting is ~10x cheaper than parsing, so the read stays and only the parse is saved. Bounded like
|
|
492
|
+
// the historical-revision memo it mirrors ([[code-anchor]]), and it caches the extractor's REJECTION too, so
|
|
493
|
+
// an unparseable file does not re-parse once per entry.
|
|
494
|
+
const CURRENT_TREE_MEMO_MAX = 4096;
|
|
495
|
+
const currentTreeUnitMemo = new Map();
|
|
496
|
+
// The memo key doubles as the IMAGE's name — the extractor that answered plus the exact bytes it read — so a
|
|
497
|
+
// caller retaining a verdict derived from these units names it by the same identity that decides the parse
|
|
498
|
+
// ([[selector-anchor-scope]]). Returning the key rather than re-deriving it elsewhere is what keeps the two
|
|
499
|
+
// from drifting apart.
|
|
500
|
+
function currentTreeImage(root, x, path) {
|
|
501
|
+
const source = readFileSync(join(root, path), 'utf8');
|
|
502
|
+
const key = `${x.memoKey(path)}\0${createHash('sha1').update(source).digest('hex')}`;
|
|
503
|
+
const hit = currentTreeUnitMemo.get(key);
|
|
504
|
+
if (hit) {
|
|
505
|
+
if ('failed' in hit)
|
|
506
|
+
throw new Error(hit.failed);
|
|
507
|
+
return { key, units: hit.units };
|
|
508
|
+
}
|
|
509
|
+
let entry;
|
|
510
|
+
try {
|
|
511
|
+
entry = { units: x.extract(source, path) };
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
entry = { failed: err?.message ?? String(err) };
|
|
515
|
+
}
|
|
516
|
+
if (currentTreeUnitMemo.size >= CURRENT_TREE_MEMO_MAX)
|
|
517
|
+
currentTreeUnitMemo.clear();
|
|
518
|
+
currentTreeUnitMemo.set(key, entry);
|
|
519
|
+
if ('failed' in entry)
|
|
520
|
+
throw new Error(entry.failed);
|
|
521
|
+
return { key, units: entry.units };
|
|
522
|
+
}
|
|
523
|
+
function entryImage(root, regs, path, selector) {
|
|
524
|
+
const x = extractorFor(regs, extOf(path));
|
|
525
|
+
if (!x)
|
|
526
|
+
return { problem: `\`code\` selector \`${path}#${selector}\` — no designated extractor for that language; drop the #anchor or add a language row` };
|
|
527
|
+
const ready = x.ready();
|
|
528
|
+
if (ready !== true)
|
|
529
|
+
return { problem: `\`code\` anchors on ${path} are unverified: ${ready}` };
|
|
530
|
+
try {
|
|
531
|
+
return currentTreeImage(root, x, path);
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
return { problem: `\`code\` anchors on ${path} are unverified: ${err?.message ?? String(err)}` };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// every selector of one entry resolves to exactly one unit in the CURRENT tree, or the entry cannot testify.
|
|
538
|
+
// This gate is what stops a DEAD selector from reading fresh: the hit engine answers "no commit touched a
|
|
539
|
+
// unit of that name", which for a name that exists nowhere is a vacuous no — true of spec drift, where the
|
|
540
|
+
// dead anchor is a separate blocking error, and dangerously false here, where the same silence would retire
|
|
541
|
+
// a reading's whole code axis. `problem` names the repair for lint; null means the entry is verifiable.
|
|
542
|
+
function selectorProblem(units, path, selectors) {
|
|
543
|
+
// the SAME classifier the gate uses ([[code-anchor]]'s resolveSelectors) — only the wording is ours, so a
|
|
544
|
+
// selector can never be verifiable to one reader and dead to the other.
|
|
545
|
+
for (const r of resolveSelectors(units, selectors)) {
|
|
546
|
+
if ('dead' in r)
|
|
547
|
+
return `\`code\` selector \`${path}#${r.selector}\` names no unit in that file — follow the rename or drop the selector (evals stay stale until then)`;
|
|
548
|
+
if ('ambiguous' in r)
|
|
549
|
+
return `\`code\` selector \`${path}#${r.selector}\` is ambiguous — ${r.ambiguous} units share that name; pin a unique one`;
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
function entryUnverifiable(root, regs, entry) {
|
|
554
|
+
const image = entryImage(root, regs, entry.path, entry.selectors[0]);
|
|
555
|
+
return 'problem' in image ? image.problem : selectorProblem(image.units, entry.path, entry.selectors);
|
|
556
|
+
}
|
|
557
|
+
// @@@ the verify sweep must reach the MACROTASK queue - resolving every reading's `code:` selectors against
|
|
558
|
+
// the working tree is a doubly-nested SYNCHRONOUS sweep (4,384 demands on this corpus), and nothing in it
|
|
559
|
+
// awaits, so it ran as one uninterruptible stretch: measured, it held the loop for 1,104ms, which is the
|
|
560
|
+
// `/health` p99 this gate is judged by. The bound defended here is a LIVENESS signal, not a latency taste —
|
|
561
|
+
// a probe that cannot answer is indistinguishable from a dead backend, and the CLI allows it 600ms while the
|
|
562
|
+
// supervisor allows 1,000ms before it keeps the old child. `setImmediate` is what actually returns to the
|
|
563
|
+
// loop's I/O phase (awaiting a synchronous-bodied async fn only drains microtasks). A time budget rather
|
|
564
|
+
// than every-iteration keeps the common cheap iteration from paying a turn: the sweep's own longest single
|
|
565
|
+
// step is one file parse, so the worst hold stays near budget + that step.
|
|
566
|
+
const VERIFY_YIELD_BUDGET_MS = Number(process.env.SPEXCODE_VERIFY_YIELD_BUDGET_MS || 50);
|
|
567
|
+
const yieldToEventLoop = () => new Promise((resolve) => { setImmediate(resolve); });
|
|
568
|
+
// A verdict's window is `sinceSha..HEAD`, and that window LENGTHENS as HEAD advances — the same
|
|
569
|
+
// (sinceSha, path, selectors) that answers false at one head can legitimately answer true at the next. So
|
|
570
|
+
// these are per-head immutable, never content-addressed-forever, and they belong in the root's current-head
|
|
571
|
+
// scope above rather than in a memo of their own ([[selector-anchor-scope]]).
|
|
572
|
+
// The working tree, however, does NOT stand still at one head: a session re-images the files a selector
|
|
573
|
+
// resolves against, and each image is its own key. Bound that with the same drop-wholesale idiom the
|
|
574
|
+
// current-tree parse memo uses instead of inventing a second eviction policy.
|
|
575
|
+
const ANCHOR_VERDICT_MAX = Math.max(1024, Number(process.env.SPEXCODE_ANCHOR_VERDICT_SLOTS || 8192));
|
|
576
|
+
export function anchorProbeFor(root, idx) {
|
|
577
|
+
const rootKey = resolve(root);
|
|
578
|
+
const regs = extractors(root);
|
|
579
|
+
const verdicts = new Map();
|
|
580
|
+
// The scope is the one the content probe already owns, entered only when the index's own tip IS the
|
|
581
|
+
// current head. An index built at another tip cannot borrow this head's answers, and — the reason this is
|
|
582
|
+
// a guard rather than a rotation — a head that moved mid-read must not make THIS probe displace the
|
|
583
|
+
// content probe's just-settled scope. Either the two probes agree on the head and share one scope, or this
|
|
584
|
+
// one keeps nothing and recomputes.
|
|
585
|
+
const scopeOf = () => {
|
|
586
|
+
if (!idx.tip)
|
|
587
|
+
return null;
|
|
588
|
+
const rawHead = headSha(root);
|
|
589
|
+
if (idx.tip !== rawHead)
|
|
590
|
+
return null;
|
|
591
|
+
return scopeFor(rootKey, `${rawHead}\x1f${gitObjectInterpretation(root).identity}`, null);
|
|
592
|
+
};
|
|
593
|
+
// which extractor BUILD answered is part of a verdict's identity too — a host TypeScript resolving to a
|
|
594
|
+
// different module or version must not inherit the previous one's units.
|
|
595
|
+
const registry = regs.map((x) => x.memoKey('\0registry')).join('\x1e');
|
|
596
|
+
return {
|
|
597
|
+
async prime(demands) {
|
|
598
|
+
const scope = scopeOf();
|
|
599
|
+
const keys = [];
|
|
600
|
+
const scoped = [];
|
|
601
|
+
const queries = [];
|
|
602
|
+
const queued = new Set();
|
|
603
|
+
// one coherent read of each source per sweep: every entry on a path resolves against the same bytes,
|
|
604
|
+
// and the read that names the verdict is the read the verdict was derived from.
|
|
605
|
+
const images = new Map();
|
|
606
|
+
const imageOf = (path, selector) => {
|
|
607
|
+
let hit = images.get(path);
|
|
608
|
+
if (hit === undefined) {
|
|
609
|
+
hit = entryImage(root, regs, path, selector);
|
|
610
|
+
images.set(path, hit);
|
|
611
|
+
}
|
|
612
|
+
return hit;
|
|
613
|
+
};
|
|
614
|
+
const remember = (name, hit) => {
|
|
615
|
+
if (scope && name !== null) {
|
|
616
|
+
if (scope.anchorVerdicts.size >= ANCHOR_VERDICT_MAX)
|
|
617
|
+
scope.anchorVerdicts.clear();
|
|
618
|
+
scope.anchorVerdicts.set(name, hit);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
let sinceYield = Date.now();
|
|
622
|
+
for (const { sinceSha, entries } of demands)
|
|
623
|
+
for (const e of entries) {
|
|
624
|
+
if (Date.now() - sinceYield >= VERIFY_YIELD_BUDGET_MS) {
|
|
625
|
+
await yieldToEventLoop();
|
|
626
|
+
sinceYield = Date.now();
|
|
627
|
+
}
|
|
628
|
+
if (!e.selectors.length)
|
|
629
|
+
continue;
|
|
630
|
+
const key = anchorKey(sinceSha, e.path, e.selectors);
|
|
631
|
+
if (verdicts.has(key) || queued.has(key))
|
|
632
|
+
continue;
|
|
633
|
+
const image = imageOf(e.path, e.selectors[0]);
|
|
634
|
+
const name = 'problem' in image ? null : `${registry}\x1f${image.key}\x1f${key}`;
|
|
635
|
+
const remembered = scope && name !== null ? scope.anchorVerdicts.get(name) : undefined;
|
|
636
|
+
if (remembered !== undefined) {
|
|
637
|
+
verdicts.set(key, remembered);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
// a miss runs the FULL derivation below, unchanged — it is what a verdict MEANS, not a fallback
|
|
641
|
+
if ('problem' in image || selectorProblem(image.units, e.path, e.selectors))
|
|
642
|
+
continue; // no verdict → conservative stale (lint says why)
|
|
643
|
+
const win = eventsSince(idx, sinceSha, e.path);
|
|
644
|
+
if (win === null)
|
|
645
|
+
continue;
|
|
646
|
+
if (!win.length) {
|
|
647
|
+
verdicts.set(key, false);
|
|
648
|
+
remember(name, false);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
queued.add(key);
|
|
652
|
+
keys.push(key);
|
|
653
|
+
scoped.push(name);
|
|
654
|
+
queries.push({ win, symbols: [...e.selectors] });
|
|
655
|
+
}
|
|
656
|
+
if (!queries.length)
|
|
657
|
+
return;
|
|
658
|
+
const results = await anchorHitExists(root, queries, regs);
|
|
659
|
+
results.forEach((hit, index) => { verdicts.set(keys[index], hit); remember(scoped[index], hit); });
|
|
660
|
+
},
|
|
661
|
+
hit(sinceSha, path, selectors) {
|
|
662
|
+
return verdicts.get(anchorKey(sinceSha, path, selectors)) ?? null;
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
// the LOUD half: a selector is a claim that a named unit EXISTS, held to the same standard as a ghost path.
|
|
667
|
+
// Dead, ambiguous, unparseable, or no designated extractor — each names itself and its repair, and until
|
|
668
|
+
// repaired the probe issues no verdict, so the reading stays stale. Over-warn, never a silent pass.
|
|
669
|
+
export function anchorProblems(root, entries) {
|
|
670
|
+
const regs = extractors(root);
|
|
671
|
+
const out = [];
|
|
672
|
+
for (const e of entries) {
|
|
673
|
+
if (!e.selectors.length)
|
|
674
|
+
continue;
|
|
675
|
+
const problem = entryUnverifiable(root, regs, e);
|
|
676
|
+
if (problem)
|
|
677
|
+
out.push(problem);
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
// the teeth (T1): a scenario is remark-stale unless EVERY remark is resolved AND this reading post-dates
|
|
682
|
+
// every resolution. So an UNRESOLVED remark ages it; a RESOLVED remark keeps it stale until a reading taken
|
|
683
|
+
// strictly after the resolve (reading.ts > resolvedAt) exists — you can't out-run a remark by re-measuring
|
|
684
|
+
// before the resolve, nor clear it by passive receipt. A resolved bit with no timestamp stays conservatively
|
|
685
|
+
// stale (defensive: resolveRemark always stamps one).
|
|
686
|
+
export function remarkStale(reading, remarks) {
|
|
687
|
+
return remarks.some((r) => !r.resolved || !(r.resolvedAt && reading.ts > r.resolvedAt));
|
|
688
|
+
}
|
|
689
|
+
// true iff some commit touched `path` that is NOT an ancestor of `sinceSha` — i.e. it lies in
|
|
690
|
+
// `sinceSha..HEAD` by true DAG reachability, never a log-position/date compare (which under-reports on
|
|
691
|
+
// branchy history). An off-history `sinceSha` falls back to the content probe when one is fed (see
|
|
692
|
+
// ContentProbe above); without a probe — or when the anchor object is gone — freshness can't be proven
|
|
693
|
+
// from HEAD's history, so it reads stale rather than silently pass.
|
|
694
|
+
export function changedSince(idx, sinceSha, path, probe) {
|
|
695
|
+
const events = eventsSince(idx, sinceSha, path);
|
|
696
|
+
// null = ancestry cannot testify for this anchor; only then does content get a say ([[root-lru]]'s sibling
|
|
697
|
+
// rule: one meaning of changed-since, each layer's own fallback on top).
|
|
698
|
+
if (events)
|
|
699
|
+
return events.length > 0;
|
|
700
|
+
return probe?.changed(sinceSha, path) ?? true;
|
|
701
|
+
}
|
|
702
|
+
// the code axis's DISPLAY detail: which governed files drifted since a reading, and by HOW MANY commits — so
|
|
703
|
+
// a stale eval can say "EvalsFeed.jsx +3" instead of a bare "code moved". Same DAG reachability as
|
|
704
|
+
// changedSince (a commit touching the file that is NOT an ancestor of the reading's sha lies in sinceSha..HEAD);
|
|
705
|
+
// an off-history sinceSha reports through the same content fallback (only files whose content differs, counted
|
|
706
|
+
// against the anchor's own ancestry); with no probe or a gone anchor it counts every touch (conservative,
|
|
707
|
+
// matching changedSince).
|
|
708
|
+
// Reporting only — it never decides freshness (staleAxes does); it explains a decision already made.
|
|
709
|
+
export function codeDrift(idx, sinceSha, codeAxis, probe) {
|
|
710
|
+
// an entry may be anchored (`path#symbol`); drift is reported per BASE FILE — a raw selector string names
|
|
711
|
+
// no real path, so counting commits against it would silently report nothing.
|
|
712
|
+
const codeFiles = scenarioCodeAxis(undefined, codeAxis).paths;
|
|
713
|
+
const out = [];
|
|
714
|
+
for (const f of codeFiles) {
|
|
715
|
+
const since = eventsSince(idx, sinceSha, f);
|
|
716
|
+
const events = pathEvents(idx, f);
|
|
717
|
+
const differs = since ? undefined : probe?.changed(sinceSha, f);
|
|
718
|
+
const behind = since ? new Set(since.map((event) => event.commit)).size
|
|
719
|
+
// off-history and the content differs: the SAME rule as the in-history branch above — the path's events
|
|
720
|
+
// this anchor has not already seen — read against the anchor's own ancestry instead of HEAD's. Floored
|
|
721
|
+
// at one: the trees demonstrably differ, so 'drifted by 0 commits' would be a lie whatever the walk
|
|
722
|
+
// could and could not project.
|
|
723
|
+
: differs === true ? Math.max(1, new Set(events.filter((event) => !probe.inAnchorPast(sinceSha, event.commit)).map((event) => event.commit)).size)
|
|
724
|
+
: differs === false ? 0
|
|
725
|
+
: new Set(events.map((event) => event.commit)).size;
|
|
726
|
+
if (behind > 0)
|
|
727
|
+
out.push({ file: f, behind });
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
// @@@scenario axis decides by stored contract hash - a reading filed since #61 carries `scenarioHash`, the
|
|
732
|
+
// content hash of the semantic text it was measured against (scenarios.ts scenarioHash — normalized
|
|
733
|
+
// description+expected). For such a reading the scenario axis is a PURE TEXT COMPARE: recorded hash vs the
|
|
734
|
+
// CURRENT declaration's hash — no git walk, no chain, no ancestry. That is what makes it converge under
|
|
735
|
+
// fleet-parallel filing+merging: a sibling scenario's edit, a sidecar-only commit, a merge's textual shift,
|
|
736
|
+
// a whitespace re-wrap — none of them move THIS scenario's hash, so none can re-stale its reading; only the
|
|
737
|
+
// contract actually changing (or the scenario disappearing from eval.md — `current` undefined) does. A
|
|
738
|
+
// LEGACY reading without the hash is decided by the git-derived per-scenario rule below, unchanged — the
|
|
739
|
+
// one-shot degradation: exactly ONE track decides each reading (hash if present, else git), never both
|
|
740
|
+
// OR-ed together.
|
|
741
|
+
function scenarioStaleByHash(reading, current) {
|
|
742
|
+
if (reading.scenarioHash === undefined)
|
|
743
|
+
return undefined; // legacy → the git rule decides
|
|
744
|
+
return current ? scenarioHash(current) !== reading.scenarioHash : true;
|
|
745
|
+
}
|
|
746
|
+
// LEGACY scenario freshness (readings filed before the stored contract hash) — PER-SCENARIO and SEMANTIC,
|
|
747
|
+
// not per-file: a reading stales only when ITS OWN scenario's semantic block (description+expected —
|
|
748
|
+
// [[scenariofresh]]'s blockContent projection) moved in scenarioSha..HEAD — never when a sibling in the
|
|
749
|
+
// same eval.md did, and never on a metadata-only edit (tags/test/code/related). Reads like the code axis's
|
|
750
|
+
// changedSince — the per-scenario change-commits ([[scenariofresh]], rename-followed so a bare git-mv
|
|
751
|
+
// reparent isn't a change) tested for ancestry — and an off-history codeSha takes the same content fallback
|
|
752
|
+
// at the same granularity and the same projection. Known limit (#61, why the hash replaced it for new
|
|
753
|
+
// readings): the change-commit chain is built off a LINEARIZED log walk, so parallel branches editing one
|
|
754
|
+
// eval.md cross-attribute each other's edits and can false-stale a sibling's reading across a merge.
|
|
755
|
+
function scenarioMoved(scIdx, didx, sinceSha, evalPath, scenario, probe) {
|
|
756
|
+
const anc = ancestorsOf(didx, sinceSha);
|
|
757
|
+
if (anc)
|
|
758
|
+
return scenarioChangeCommits(scIdx, evalPath, scenario).some((h) => !inAncestors(didx, anc, h));
|
|
759
|
+
const differs = probe?.changed(sinceSha, evalPath);
|
|
760
|
+
if (differs == null)
|
|
761
|
+
return true;
|
|
762
|
+
if (!differs)
|
|
763
|
+
return false; // whole file byte-identical → this block too
|
|
764
|
+
return probe.scenarioDiffers(sinceSha, evalPath, scenario);
|
|
765
|
+
}
|
|
766
|
+
// one declared entry's contribution to the code axis. The FILE question runs first and is unchanged; only an
|
|
767
|
+
// ANCHORED entry whose file really moved asks the narrower spatial one, so an anchor can subtract from the
|
|
768
|
+
// file verdict but never add to it. No verdict (unprimed, off-history, unverifiable selector) → conservative.
|
|
769
|
+
function entryMoved(idx, sinceSha, entry, probe, anchors) {
|
|
770
|
+
if (!changedSince(idx, sinceSha, entry.path, probe))
|
|
771
|
+
return false;
|
|
772
|
+
if (!entry.selectors.length)
|
|
773
|
+
return true;
|
|
774
|
+
return anchors?.hit(sinceSha, entry.path, entry.selectors) ?? true;
|
|
775
|
+
}
|
|
776
|
+
export function staleAxes(reading, codeAxis, evalPath, didx, scIdx, remarks = [], probe, current, // the scenario's CURRENT declaration (undefined = gone from eval.md) — the hash compare's other side
|
|
777
|
+
anchors) {
|
|
778
|
+
const axes = [];
|
|
779
|
+
const byHash = scenarioStaleByHash(reading, current);
|
|
780
|
+
if (probe && !commitReachable(didx, reading.codeSha) && !probe.canTestify(reading.codeSha)) {
|
|
781
|
+
// the anchor commit object is GONE — neither git axis can testify; say that, not "content changed".
|
|
782
|
+
// The stored contract hash needs no anchor, so it still decides the scenario axis when present.
|
|
783
|
+
axes.push('anchor');
|
|
784
|
+
if (byHash)
|
|
785
|
+
axes.push('scenario');
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
if (scenarioCodeAxis(undefined, codeAxis).entries.some((e) => entryMoved(didx, reading.codeSha, e, probe, anchors)))
|
|
789
|
+
axes.push('code');
|
|
790
|
+
if (byHash ?? scenarioMoved(scIdx, didx, reading.codeSha, evalPath, reading.scenario, probe))
|
|
791
|
+
axes.push('scenario');
|
|
792
|
+
}
|
|
793
|
+
if (remarkStale(reading, remarks))
|
|
794
|
+
axes.push('remark');
|
|
795
|
+
return axes;
|
|
796
|
+
}
|
|
797
|
+
export function isStale(reading, codeAxis, evalPath, didx, scIdx, remarks = [], probe, current, anchors) {
|
|
798
|
+
return staleAxes(reading, codeAxis, evalPath, didx, scIdx, remarks, probe, current, anchors).length > 0;
|
|
799
|
+
}
|