spexcode 0.6.2 → 0.6.6
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 +3 -1
- package/bin/spex.mjs +4 -0
- package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +195 -0
- package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/session-core/dist/index.js +5 -0
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
- package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
- package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
- package/node_modules/@spexcode/session-core/dist/message.js +52 -0
- package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
- package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +50 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +286 -0
- package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +14 -0
- package/node_modules/@spexcode/session-core/dist/session-cursors.js +82 -0
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +46 -0
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
- package/node_modules/@spexcode/session-core/package.json +33 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +77 -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 +36 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +316 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1908 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +112 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +481 -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 +58 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +798 -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/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +553 -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 +363 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +38 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +527 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +717 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +36 -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} +36 -32
- 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 +338 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3146 -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 +15 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +594 -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 +8 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.js +24 -0
- 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 +5 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +215 -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/runtime-rotate.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -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-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 +11 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +20 -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 +433 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4667 -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 +1 -1
- package/node_modules/@spexcode/spec-cli/package.json +49 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -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/{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-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 +170 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2768 -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 +187 -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 -728
- package/spec-cli/src/attach.ts +0 -43
- package/spec-cli/src/claude-headless.ts +0 -321
- package/spec-cli/src/cli.ts +0 -1258
- package/spec-cli/src/client.ts +0 -414
- 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 -165
- package/spec-cli/src/doctor.ts +0 -516
- package/spec-cli/src/execution-trace.ts +0 -444
- 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 -358
- package/spec-cli/src/gateway.ts +0 -461
- package/spec-cli/src/git.ts +0 -2556
- package/spec-cli/src/graph.ts +0 -271
- package/spec-cli/src/graphCache.ts +0 -710
- 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 -3025
- package/spec-cli/src/headless-controller.ts +0 -63
- package/spec-cli/src/help.ts +0 -492
- package/spec-cli/src/host-resources.ts +0 -762
- 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 -138
- package/spec-cli/src/index.ts +0 -818
- 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 -640
- package/spec-cli/src/lint.ts +0 -488
- package/spec-cli/src/listen.ts +0 -28
- package/spec-cli/src/localIssues.ts +0 -538
- package/spec-cli/src/materialize.ts +0 -417
- 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 -434
- 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-execution.ts +0 -68
- package/spec-cli/src/session-files.ts +0 -121
- package/spec-cli/src/session-follow.ts +0 -182
- package/spec-cli/src/session-reparent.ts +0 -33
- package/spec-cli/src/session-timeline.ts +0 -247
- package/spec-cli/src/session-web.ts +0 -135
- package/spec-cli/src/sessions.ts +0 -3883
- 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-dashboard/dist/assets/App-B6IbFbGu.js +0 -2
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
- package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-BG9rNOgV.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-D9yPMR3A.js +0 -2
- package/spec-dashboard/dist/assets/Modal-B3EcgeA3.js +0 -1
- package/spec-dashboard/dist/assets/PageScroll-CNZOugWV.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-C2CRHmvs.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
- package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +0 -40
- package/spec-dashboard/dist/assets/SessionWindow-1nYgZ4CL.js +0 -1
- package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +0 -1
- package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +0 -13
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
- package/spec-dashboard/dist/assets/data-SlvxNA2r.js +0 -1
- package/spec-dashboard/dist/assets/index-DLVeOHL-.js +0 -41
- package/spec-dashboard/dist/assets/index-DYLGfPjx.css +0 -1
- package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
- package/spec-dashboard/dist/assets/launch-yxCYr64x.js +0 -276
- package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.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 -894
- package/spec-eval/src/evaltab.ts +0 -264
- package/spec-eval/src/filing.ts +0 -39
- package/spec-eval/src/freshness.ts +0 -836
- 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 -2220
- 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/comment-altitude/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/core/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/reproduce-before-fix/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
package/spec-eval/src/cli.ts
DELETED
|
@@ -1,894 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
-
import { join, relative, dirname } from 'node:path'
|
|
3
|
-
import { repoRoot, headSha, driftIndex, stagedFiles, git, commitReachable } from '../../spec-cli/src/git.js'
|
|
4
|
-
import { loadSpecs } from '../../spec-cli/src/specs.js'
|
|
5
|
-
import { loadConfig } from '../../spec-cli/src/lint.js'
|
|
6
|
-
import { trackedSourceFiles } from '../../spec-cli/src/source-files.js'
|
|
7
|
-
import { mainBranch, envSessionId, readRawRecord } from '../../spec-cli/src/layout.js'
|
|
8
|
-
import { evalNodes, evalNodesAt, validateScenarios, resolveEvalNode, scenarioCodeAxis, scenarioHash, scenarioProjection, writeScenarioMeasurementMetadata, EVAL_FILE, type EvalNode, type ScenarioTestReference } from './scenarios.js'
|
|
9
|
-
import { readReadings, readSidecar, appendReading, appendRetraction, latestPerScenario, evidenceOf, isJsonBlob, type Reading, type Verdict, type Evidence, type EvidenceKind, type Retraction } from './sidecar.js'
|
|
10
|
-
import { staleAxes, contentProbeFor, anchorProbeFor, anchorProblems, type AnchorDemand, type ContentProbeDemand } from './freshness.js'
|
|
11
|
-
import { parseRelation, relationClaimsPath } from '../../spec-cli/src/anchors.js'
|
|
12
|
-
import { scenarioIndex } from './scenariofresh.js'
|
|
13
|
-
import { loadEvalRemarkTracks, trackKey } from '../../spec-cli/src/issues.js'
|
|
14
|
-
import { stripRefSigil } from '../../spec-cli/src/mentions.js'
|
|
15
|
-
import { putBlob, blobPath, listBlobs, gc, isStrayBlob } from './cache.js'
|
|
16
|
-
import { validateTimeline, normalizeTimeline } from './timeline.js'
|
|
17
|
-
import { evalTimeline, readBlobByHash, type EvalTimeline } from './evaltab.js'
|
|
18
|
-
|
|
19
|
-
function flag(args: string[], name: string): string | undefined {
|
|
20
|
-
const i = args.indexOf(`--${name}`)
|
|
21
|
-
return i >= 0 ? args[i + 1] : undefined
|
|
22
|
-
}
|
|
23
|
-
// every value of a REPEATABLE flag (e.g. `--image a --image b`), in argv order; a trailing `--image` with no
|
|
24
|
-
// value (or another flag as its value) is dropped so a typo can't swallow the next flag.
|
|
25
|
-
function flags(args: string[], name: string): string[] {
|
|
26
|
-
const out: string[] = []
|
|
27
|
-
for (let i = 0; i < args.length; i++) if (args[i] === `--${name}`) { const v = args[i + 1]; if (v !== undefined && !v.startsWith('--')) out.push(v) }
|
|
28
|
-
return out
|
|
29
|
-
}
|
|
30
|
-
const has = (args: string[], name: string) => args.includes(`--${name}`)
|
|
31
|
-
const positional = (args: string[]) => args.find((a) => !a.startsWith('--'))
|
|
32
|
-
|
|
33
|
-
// join by node directory (not id) so a reparent/rename is seen the way lint sees it
|
|
34
|
-
type ScoredNode = EvalNode & { codeFiles: string[] }
|
|
35
|
-
|
|
36
|
-
async function gatherNodes(root: string): Promise<ScoredNode[]> {
|
|
37
|
-
const specs = await loadSpecs()
|
|
38
|
-
const codeByDir = new Map<string, string[]>()
|
|
39
|
-
for (const s of specs) codeByDir.set(dirname(s.path), s.code) // s.path = repo-relative spec.md path
|
|
40
|
-
return evalNodes(root).map((n) => ({ ...n, codeFiles: codeByDir.get(relative(root, n.dir)) ?? [] }))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// resolve `.` → the node this worktree works on: the session record's `node` (the authoritative ref a
|
|
44
|
-
// dashboard session was bound to — NOT derivable from the branch, whose slug carries a `-<id4>` suffix),
|
|
45
|
-
// else the `node/<id>` branch. The record now lives in the GLOBAL store keyed by the harness session id
|
|
46
|
-
// ([[state]]), so we read it via the env session id; a self-launched agent with no record falls back to the branch.
|
|
47
|
-
function currentNodeId(root: string): string | null {
|
|
48
|
-
const id = envSessionId()
|
|
49
|
-
if (id) { const rec = readRawRecord(id); if (rec?.node) return rec.node }
|
|
50
|
-
try {
|
|
51
|
-
const branch = git(['-C', root, 'symbolic-ref', '--short', 'HEAD']).trim()
|
|
52
|
-
if (branch.startsWith('node/')) return branch.slice('node/'.length)
|
|
53
|
-
} catch { /* detached / no branch */ }
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// isUiPath answers a FRONTEND-specific question — "does this node need a real BROWSER reading?" — and is
|
|
58
|
-
// consumed by the session-eval's `uncoveredFrontend` blindspot, NOT by eval lint's coverage check. Scan uses
|
|
59
|
-
// the shared tracked-source classifier instead, so a non-web project's sources are held to the loss discipline
|
|
60
|
-
// too; this stays web-shaped on purpose.
|
|
61
|
-
const UI_FILE = /\.(jsx|tsx|vue|svelte|css)$/
|
|
62
|
-
export const isUiPath = (p: string) => UI_FILE.test(p) || p.includes('spec-dashboard/')
|
|
63
|
-
|
|
64
|
-
type ChangedScope = { base: string; paths: Set<string>; config: string }
|
|
65
|
-
|
|
66
|
-
// Merge-base anchored so main advancing is not counted as this branch's change. Scope construction is
|
|
67
|
-
// indivisible: a missing base/diff is no scope at all, never an empty set that can masquerade as clean.
|
|
68
|
-
function changedScope(root: string): ChangedScope {
|
|
69
|
-
const branch = mainBranch()
|
|
70
|
-
const paths = new Set<string>()
|
|
71
|
-
const add = (path: string | undefined) => { if (path) paths.add(path) }
|
|
72
|
-
try {
|
|
73
|
-
const base = git(['-C', root, 'merge-base', branch, 'HEAD']).trim()
|
|
74
|
-
if (!base) throw new Error('git merge-base returned no commit')
|
|
75
|
-
const fields = git(['-C', root, '-c', 'core.quotePath=false', 'diff', '--name-status', '-z', '-M', base])
|
|
76
|
-
.split('\0').filter(Boolean)
|
|
77
|
-
for (let i = 0; i < fields.length;) {
|
|
78
|
-
const status = fields[i++]
|
|
79
|
-
if (/^[RC]/.test(status)) { add(fields[i++]); add(fields[i++]) }
|
|
80
|
-
else add(fields[i++])
|
|
81
|
-
}
|
|
82
|
-
for (const path of git(['-C', root, '-c', 'core.quotePath=false', 'ls-files', '--others', '--exclude-standard']).split('\n')) add(path.trim())
|
|
83
|
-
const config = existsSync(join(root, 'spexcode.json')) ? join(root, 'spexcode.json') : 'defaults'
|
|
84
|
-
return { base, paths, config }
|
|
85
|
-
} catch (error) {
|
|
86
|
-
const detail = error instanceof Error ? error.message.trim() : String(error)
|
|
87
|
-
throw new Error(`cannot establish changed scope against '${branch}': ${detail}`)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// A node owns the files under its spec dir except those under a descendant node. Its other selection axis
|
|
92
|
-
// is the supplied code list — node code for per-node findings, effective scenario code for drift.
|
|
93
|
-
export function nodeChanged(dirRel: string, codeFiles: readonly string[], changed: Set<string>, nodeDirs: Iterable<string> = []): boolean {
|
|
94
|
-
const descendants = [...nodeDirs].filter((d) => d !== dirRel && d.startsWith(dirRel + '/'))
|
|
95
|
-
for (const c of changed) {
|
|
96
|
-
const inNodeDir = c === dirRel || c.startsWith(dirRel + '/')
|
|
97
|
-
const inDescendant = descendants.some((d) => c === d || c.startsWith(d + '/'))
|
|
98
|
-
if (inNodeDir && !inDescendant) return true
|
|
99
|
-
}
|
|
100
|
-
return codeFiles.some((claim) => [...changed].some((file) => relationClaimsPath(claim, file)))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function scan(args: string[] = []): Promise<number> {
|
|
104
|
-
const root = repoRoot()
|
|
105
|
-
const cfg = loadConfig(root)
|
|
106
|
-
const changedOnly = has(args, 'changed')
|
|
107
|
-
let changed: Set<string> | null = null
|
|
108
|
-
if (changedOnly) {
|
|
109
|
-
try {
|
|
110
|
-
const scope = changedScope(root)
|
|
111
|
-
changed = scope.paths
|
|
112
|
-
console.error(`spex eval lint --changed scope: base=${scope.base} paths=${scope.paths.size} config=${scope.config}`)
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error(`spex eval lint --changed: ${error instanceof Error ? error.message : String(error)}`)
|
|
115
|
-
return 2
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// eval-coverage fires on ANY governed source file per the same classifier as spec coverage, not just
|
|
119
|
-
// frontend — so backend/CLI/non-web source is held to the loss discipline too, not exempted.
|
|
120
|
-
const sourceFiles = new Set(trackedSourceFiles(root, cfg.governedRoots, cfg))
|
|
121
|
-
const idx = await driftIndex(root)
|
|
122
|
-
// the off-history content fallback ([[eval-core]]): a rebased/folded-away anchor with byte-identical
|
|
123
|
-
// governed content reads fresh instead of false-positive stale. Lazy — in-history readings never probe.
|
|
124
|
-
const probe = contentProbeFor(root)
|
|
125
|
-
// the code axis's spatial narrowing ([[eval-core]]): an anchored `code:` entry stales a reading only when a
|
|
126
|
-
// window commit touched one of its named units. Same lazy shape as the content probe — a bare entry never
|
|
127
|
-
// reaches it, so an unanchored corpus pays nothing.
|
|
128
|
-
const anchors = anchorProbeFor(root, idx)
|
|
129
|
-
const scidx = await scenarioIndex(root, evalNodes(root).map((n) => n.evalPath))
|
|
130
|
-
const specs = await loadSpecs()
|
|
131
|
-
// the non-git REMARK freshness axis ([[remark-teeth]]): the trunk remark track, read ONCE — the CLI is the
|
|
132
|
-
// whole model, so `spex eval lint` shows a remark-stale scenario with no server running.
|
|
133
|
-
const remarkTracks = loadEvalRemarkTracks()
|
|
134
|
-
// a file may be governed by several nodes — ordinary composition, not a hub to skip (see governed-related).
|
|
135
|
-
// A change to a shared governed file legitimately triggers EVERY governing node's eval signal, mirroring how
|
|
136
|
-
// lint's drift now fans to every owner; nobody's loss signal is suppressed. An over-owned file is lint's
|
|
137
|
-
// `owners` concern (split it), not a reason to go silent here.
|
|
138
|
-
const yByDir = new Map(evalNodes(root).map((n) => [relative(root, n.dir), n]))
|
|
139
|
-
const nodeDirs = specs.map((s) => dirname(s.path))
|
|
140
|
-
let flaggedNodes = 0, malformed = 0, staleScores = 0, missingScores = 0, uncovered = 0, danglingTracks = 0
|
|
141
|
-
// One anchor prime for the whole scan. Every window's Git images and hunks are immutable facts the engine
|
|
142
|
-
// owns per CALL, so priming per reading re-forks that batch for every row scanned. The demand set mirrors
|
|
143
|
-
// the loop's own `driftSelected` rule exactly, so `--changed` still asks about nothing it would not judge;
|
|
144
|
-
// the in-loop primes stay as the correctness backstop and become cache hits.
|
|
145
|
-
const latestByDir = new Map<string, ReturnType<typeof latestPerScenario>>()
|
|
146
|
-
const anchorDemands: AnchorDemand[] = []
|
|
147
|
-
const contentDemands: ContentProbeDemand[] = []
|
|
148
|
-
for (const s of specs) {
|
|
149
|
-
const dirRel = dirname(s.path)
|
|
150
|
-
const y = yByDir.get(dirRel)
|
|
151
|
-
if (!y) continue
|
|
152
|
-
const latest = latestPerScenario(readReadings(y.sidecarPath))
|
|
153
|
-
latestByDir.set(dirRel, latest)
|
|
154
|
-
for (const sc of y.scenarios) {
|
|
155
|
-
const r = latest.get(sc.name)
|
|
156
|
-
if (!r) continue
|
|
157
|
-
const axis = scenarioCodeAxis(sc.code, s.codeEntries)
|
|
158
|
-
if (changed && !nodeChanged(dirRel, axis.paths, changed, nodeDirs)) continue
|
|
159
|
-
anchorDemands.push({ sinceSha: r.codeSha, entries: axis.entries })
|
|
160
|
-
if (!commitReachable(idx, r.codeSha))
|
|
161
|
-
contentDemands.push({ anchorSha: r.codeSha, paths: axis.paths, evalPath: y.evalPath })
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
await probe.primeMany?.(contentDemands)
|
|
165
|
-
await probe.primeBlocks?.()
|
|
166
|
-
await anchors.prime?.(anchorDemands)
|
|
167
|
-
for (const s of specs) {
|
|
168
|
-
const dirRel = dirname(s.path)
|
|
169
|
-
const nodeSelected = !changed || nodeChanged(dirRel, s.code, changed, nodeDirs)
|
|
170
|
-
const y = yByDir.get(dirRel)
|
|
171
|
-
const findings: string[] = []
|
|
172
|
-
if (y) {
|
|
173
|
-
// schema first: a malformed eval.md is the loudest gap — report each violation, then still scan its
|
|
174
|
-
// (leniently-parsed) scenarios for stale/missing so a typo doesn't mask a real freshness gap.
|
|
175
|
-
if (nodeSelected) {
|
|
176
|
-
for (const e of validateScenarios(readFileSync(join(y.dir, EVAL_FILE), 'utf8'), cfg.scenarioTags, root)) {
|
|
177
|
-
malformed++
|
|
178
|
-
findings.push(` • eval-schema: '${s.id}' ${e} — fix ${y.evalPath}`)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const latest = latestByDir.get(dirRel) ?? latestPerScenario(readReadings(y.sidecarPath))
|
|
182
|
-
for (const sc of y.scenarios) {
|
|
183
|
-
// a scenario's own `code` narrows its freshness CODE axis to a subset; a path that does not exist
|
|
184
|
-
// would make that axis silently immortal (changedSince finds no commits for it), so flag it LOUD as a
|
|
185
|
-
// malformed declaration — the same loud-fail spirit as a bad node `code:`.
|
|
186
|
-
// ONE resolution of the axis: base paths for every path consumer (existence, changed-scan selection,
|
|
187
|
-
// drift display), folded selectors for the narrowing. A raw `path#symbol` matches no real file, so
|
|
188
|
-
// reading it as a path would report a ghost and drop the scenario out of --changed selection.
|
|
189
|
-
const axis = scenarioCodeAxis(sc.code, s.codeEntries)
|
|
190
|
-
if (nodeSelected) {
|
|
191
|
-
for (const [field, paths] of [['code', axis.paths], ['related', parseRelation(sc.related ?? [], 'related').entries.map((e) => e.path)]] as const) {
|
|
192
|
-
const ghosts = paths.filter((p) => !existsSync(join(root, p)))
|
|
193
|
-
if (ghosts.length) {
|
|
194
|
-
malformed++
|
|
195
|
-
findings.push(` • eval-schema: '${s.id}' scenario '${sc.name}' \`${field}\` path(s) not found: ${ghosts.join(', ')} — fix ${y.evalPath}`)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// an anchor is a claim a named unit exists — dead/ambiguous/unextractable is LOUD, and until it is
|
|
199
|
-
// repaired the probe issues no verdict, so the reading stays conservatively stale.
|
|
200
|
-
for (const p of [...axis.problems, ...anchorProblems(root, axis.entries)]) {
|
|
201
|
-
malformed++
|
|
202
|
-
findings.push(` • eval-schema: '${s.id}' scenario '${sc.name}' ${p} — fix ${y.evalPath}`)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const codeFiles = axis.paths // scenario's own subset, else the node's list
|
|
206
|
-
const driftSelected = !changed || nodeChanged(dirRel, codeFiles, changed, nodeDirs)
|
|
207
|
-
// carry the scenario's tags on the finding line — its SURFACE (e.g. frontend-e2e = browser-measured)
|
|
208
|
-
// is what routes a drift/missing gap to the right measuring hand, so the proactive nudge and a human
|
|
209
|
-
// reading `spex eval lint` both see whether this stale score needs a real e2e/browser pass to refresh.
|
|
210
|
-
const tagStr = sc.tags?.length ? ` [${sc.tags.join(',')}]` : ''
|
|
211
|
-
const r = latest.get(sc.name)
|
|
212
|
-
if (!r) {
|
|
213
|
-
if (nodeSelected) {
|
|
214
|
-
missingScores++
|
|
215
|
-
findings.push(` • eval-missing: '${s.id}' scenario '${sc.name}'${tagStr} has no eval yet — measure with \`spex eval add ${s.id}\``)
|
|
216
|
-
}
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
219
|
-
if (!driftSelected) continue
|
|
220
|
-
const remSignals = (remarkTracks.get(trackKey(s.id, sc.name))?.remarks ?? []).map((rm) => ({ resolved: !!rm.resolved, resolvedAt: rm.resolvedAt }))
|
|
221
|
-
if (!commitReachable(idx, r.codeSha)) {
|
|
222
|
-
await probe.prime?.(r.codeSha, codeFiles, y.evalPath)
|
|
223
|
-
// lint decides this reading before moving on, so its demand is flushed here rather than pooled;
|
|
224
|
-
// the batched pair still beats the per-revision lookups it replaces.
|
|
225
|
-
await probe.primeBlocks?.()
|
|
226
|
-
}
|
|
227
|
-
await anchors.prime?.([{ sinceSha: r.codeSha, entries: axis.entries }])
|
|
228
|
-
const axes = staleAxes(r, axis.entries, y.evalPath, idx, scidx, remSignals, probe, sc, anchors)
|
|
229
|
-
if (axes.length) {
|
|
230
|
-
staleScores++
|
|
231
|
-
// a remark-stale scenario is unlocked by a second-party resolve, then a fresh reading; the git axes
|
|
232
|
-
// by a re-measure. Both read the same word: "re-measure with spex eval add". The anchor axis is
|
|
233
|
-
// a LOSS, not a change — the reading's commit object no longer exists, so content can't testify.
|
|
234
|
-
const others = axes.filter((a) => a !== 'anchor')
|
|
235
|
-
const why = [
|
|
236
|
-
...(axes.includes('anchor') ? [`anchor commit ${r.codeSha.slice(0, 7)} is gone — history rewritten and pruned`] : []),
|
|
237
|
-
...(others.length ? [`${others.join(', ')} changed since ${r.codeSha.slice(0, 7)}`] : []),
|
|
238
|
-
].join('; ')
|
|
239
|
-
findings.push(` • eval-drift: '${s.id}' scenario '${sc.name}'${tagStr} is stale (${why}) — re-measure with \`spex eval add ${s.id}\``)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// DANGLING remark tracks (directive 5): a (node, scenario) remark track whose scenario is gone from
|
|
243
|
-
// eval.md AND has no reading (renamed/deleted) — its remarks would surface nowhere on the loss signal.
|
|
244
|
-
// One note per node so the orphan is visible; the remarks stay resolvable/retractable via their refs
|
|
245
|
-
// (`spex remark resolve`/`spex remark retract`), and they age nothing (there is no reading to stale).
|
|
246
|
-
const declared = new Set(y.scenarios.map((sc) => sc.name))
|
|
247
|
-
const orphans = [...remarkTracks.values()].filter((tr) => tr.node === s.id && tr.remarks.length && !declared.has(tr.scenario) && !latest.has(tr.scenario))
|
|
248
|
-
if (nodeSelected && orphans.length) {
|
|
249
|
-
danglingTracks += orphans.length
|
|
250
|
-
const names = orphans.map((o) => `'${o.scenario}' (${o.threadId}, ${o.remarks.length} remark${o.remarks.length > 1 ? 's' : ''})`).join(', ')
|
|
251
|
-
findings.push(` • eval-dangling: '${s.id}' has ${orphans.length} orphaned remark track(s) — scenario ${names} renamed/deleted; resolve/retract via \`spex remark resolve <ref>\` / \`spex remark retract <ref>\` or restore the scenario name`)
|
|
252
|
-
}
|
|
253
|
-
} else if (nodeSelected && s.code.some((p) => sourceFiles.has(p))) {
|
|
254
|
-
uncovered++
|
|
255
|
-
findings.push(` • eval-coverage: '${s.id}' governs source code but has no eval.md — give it a scenario (description + expected) so its loss can be measured`)
|
|
256
|
-
}
|
|
257
|
-
if (!findings.length) continue
|
|
258
|
-
flaggedNodes++
|
|
259
|
-
for (const f of findings) console.error(f)
|
|
260
|
-
}
|
|
261
|
-
// whole-repo only (never --changed): a structural fact, not a per-branch freshness gap. Counts only explicit scenario `code:`.
|
|
262
|
-
let overOwned = 0
|
|
263
|
-
if (!changedOnly) {
|
|
264
|
-
const maxOwners = cfg.maxOwners
|
|
265
|
-
const govCount = new Map<string, number>()
|
|
266
|
-
// scoped entries stay OUT of the bound ([[code-anchor]]): a scenario claiming named units does NOT claim
|
|
267
|
-
// the file, so narrowing an axis can never manufacture an eval-owners smell — the count stays a fact
|
|
268
|
-
// about how many scenarios measure the WHOLE file.
|
|
269
|
-
for (const n of yByDir.values()) for (const sc of n.scenarios)
|
|
270
|
-
for (const e of scenarioCodeAxis(sc.code).entries) if (!e.selectors.length) govCount.set(e.path, (govCount.get(e.path) ?? 0) + 1)
|
|
271
|
-
const over = [...govCount].filter(([, c]) => c > maxOwners).sort((a, b) => b[1] - a[1])
|
|
272
|
-
if (over.length) {
|
|
273
|
-
overOwned = over.length
|
|
274
|
-
const top = over.slice(0, 5).map(([f, c]) => `${f.split('/').pop()}(${c})`).join(', ')
|
|
275
|
-
console.error(` • eval-owners: ${over.length} file(s) are governed by > ${maxOwners} scenarios — each carries more separately-measured behaviour than one file should. Worst: ${top}. SPLIT the file so each scenario measures its own surface.`)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const scope = changedOnly ? ' --changed' : ''
|
|
279
|
-
const ownersNote = overOwned ? `, ${overOwned} over-owned` : ''
|
|
280
|
-
const danglingNote = danglingTracks ? `, ${danglingTracks} dangling` : ''
|
|
281
|
-
console.error(`spex eval lint${scope}: ${flaggedNodes} node(s) flagged (${malformed} malformed, ${staleScores} stale, ${missingScores} missing, ${uncovered} coverage gap(s)${danglingNote}${ownersNote})`)
|
|
282
|
-
return 0
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// eval's flag set is CLOSED — like the scenario schema's closed field set. An unrecognized flag is
|
|
286
|
-
// rejected LOUD, never silently ignored: an old CLI that didn't know `--video` once filed the clip as an
|
|
287
|
-
// `--image`, and a misfiled reading is worse than none (it reads as proof). Value-flags consume the next
|
|
288
|
-
// token, so a path/note that itself starts with `--` is never mistaken for a flag.
|
|
289
|
-
const EVAL_VALUE_FLAGS = new Set(['scenario', 'note', 'image', 'result', 'video', 'timeline'])
|
|
290
|
-
|
|
291
|
-
// which axis each evidence kind carries — a step-map ([[step-timeline]]) anchors to ONE of them, so its
|
|
292
|
-
// `axis` must match a present entry's kind. A video is time (ms), a transcript is line-numbered, a still
|
|
293
|
-
// SEQUENCE is frame-indexed, and structured `data`'s positions are record ordinals — the `index` axis (a
|
|
294
|
-
// bare ordinal). Render kind and step-map axis stay ORTHOGONAL ([[evidence-kind-taxonomy]]): this map is
|
|
295
|
-
// the one seam between them, one row per kind.
|
|
296
|
-
const AXIS_FOR_KIND: Record<EvidenceKind, string> = { video: 'time', transcript: 'line', image: 'frame', data: 'index' }
|
|
297
|
-
const EVAL_BOOL_FLAGS = new Set(['pass', 'fail'])
|
|
298
|
-
function rejectUnknownEvalFlag(args: string[]): string | null {
|
|
299
|
-
for (let i = 0; i < args.length; i++) {
|
|
300
|
-
const a = args[i]
|
|
301
|
-
if (!a.startsWith('--')) continue
|
|
302
|
-
const name = a.slice(2)
|
|
303
|
-
if (EVAL_VALUE_FLAGS.has(name)) { i++; continue } // its value is the next token — skip it
|
|
304
|
-
if (EVAL_BOOL_FLAGS.has(name)) continue
|
|
305
|
-
return a
|
|
306
|
-
}
|
|
307
|
-
return null
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function evalCmd(args: string[]): Promise<number> {
|
|
311
|
-
const root = repoRoot()
|
|
312
|
-
const bad = rejectUnknownEvalFlag(args)
|
|
313
|
-
if (bad) {
|
|
314
|
-
console.error(`spex eval add: unknown flag '${bad}' — accepts --scenario --pass --fail --note --image --result --video --timeline`)
|
|
315
|
-
return 2
|
|
316
|
-
}
|
|
317
|
-
const sel = positional(args)
|
|
318
|
-
const ref = !sel || sel === '.' ? currentNodeId(root) : stripRefSigil(sel) // node args tolerate @/[[ ]] sigils ([[mentions]])
|
|
319
|
-
if (!ref) { console.error('spex eval add .: no current node (no .session/node-branch here) — name a node'); return 2 }
|
|
320
|
-
// resolve LOUD ([[eval-core]]): exact canonical id, else a unique bare leaf; an ambiguous leaf lists its
|
|
321
|
-
// candidate canonical ids instead of filing against an arbitrary node.
|
|
322
|
-
const res = resolveEvalNode(await gatherNodes(root), ref)
|
|
323
|
-
if (!res.ok) { console.error(`spex eval add: ${res.error}`); return 1 }
|
|
324
|
-
const node = res.node
|
|
325
|
-
const id = node.id
|
|
326
|
-
if (!node.scenarios.length) { console.error(`spex eval add: '${id}' declares no scenarios in its eval.md`); return 1 }
|
|
327
|
-
|
|
328
|
-
// which scenario this measurement is OF: --scenario, or the sole scenario when there is exactly one.
|
|
329
|
-
const names = node.scenarios.map((s) => s.name)
|
|
330
|
-
const scName = flag(args, 'scenario')
|
|
331
|
-
let scenario = scName ? node.scenarios.find((s) => s.name === scName) : node.scenarios.length === 1 ? node.scenarios[0] : undefined
|
|
332
|
-
if (!scenario) {
|
|
333
|
-
const why = scName ? `has no scenario '${scName}'` : `declares ${node.scenarios.length} scenarios — name one with --scenario <name>`
|
|
334
|
-
console.error(`spex eval add: '${id}' ${why} — declared: ${names.join(', ')}`)
|
|
335
|
-
return 1
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// the verdict the agent reached (required — a measurement without one is the legacy shape, not a filing).
|
|
339
|
-
const verdict = parseVerdict(args)
|
|
340
|
-
if (!verdict) { console.error('spex eval add: a verdict is required — --pass or --fail (either may add --note <text>)'); return 2 }
|
|
341
|
-
|
|
342
|
-
// the evidence the agent captured (optional; a LIST now — REPEATABLE --image plus an optional --result
|
|
343
|
-
// and/or --video, in any combination). The bytes go to the content-addressed cache exactly the same
|
|
344
|
-
// whichever kind; each becomes one typed entry on the reading's `evidence` list. A --video clip is the
|
|
345
|
-
// truest evidence for a UI-surface scenario — a recording of the loop — and N stills can ride beside it.
|
|
346
|
-
const images = flags(args, 'image')
|
|
347
|
-
const result = flag(args, 'result')
|
|
348
|
-
const video = flag(args, 'video')
|
|
349
|
-
const evidence: Evidence[] = []
|
|
350
|
-
for (const p of images) evidence.push({ hash: putBlob(readFileSync(p)), kind: 'image' })
|
|
351
|
-
if (result !== undefined) {
|
|
352
|
-
// --result's kind is DERIVED FROM CONTENT ([[evidence-kind-taxonomy]]), not welded to the flag: a
|
|
353
|
-
// structured export (JSON) files as `data` (a validatable data block), free-form output as `transcript`.
|
|
354
|
-
const bytes = readFileSync(result === '-' ? 0 : result)
|
|
355
|
-
evidence.push({ hash: putBlob(bytes), kind: isJsonBlob(bytes) ? 'data' : 'transcript' })
|
|
356
|
-
}
|
|
357
|
-
if (video !== undefined) evidence.push({ hash: putBlob(readFileSync(video)), kind: 'video' })
|
|
358
|
-
|
|
359
|
-
// --timeline: the evidence's step map (timeline.ts) — steps anchored to a POSITION on the evidence's OWN
|
|
360
|
-
// axis, no longer video-welded. It accompanies ANY axis-bearing evidence, and its `axis` must MATCH a
|
|
361
|
-
// present entry's kind (a video is time, a transcript is line, a still sequence is frame — AXIS_FOR_KIND):
|
|
362
|
-
// a step-map for an axis nothing here carries is misfiled. Validated LOUD at filing (a malformed map is
|
|
363
|
-
// rejected, never silently reshaped); stored canonicalized so identical timelines share one blob.
|
|
364
|
-
const timeline = flag(args, 'timeline')
|
|
365
|
-
let timelineBlob: string | undefined
|
|
366
|
-
if (timeline !== undefined) {
|
|
367
|
-
if (!evidence.length) { console.error('spex eval add: --timeline needs axis-bearing evidence — attach the --video/--image/--result whose axis its steps anchor to'); return 2 }
|
|
368
|
-
let parsed: unknown
|
|
369
|
-
try { parsed = JSON.parse(readFileSync(timeline, 'utf8')) } catch { console.error(`spex eval add: --timeline ${timeline} is not readable JSON`); return 2 }
|
|
370
|
-
const terrs = validateTimeline(parsed)
|
|
371
|
-
if (terrs.length) {
|
|
372
|
-
console.error('spex eval add: invalid step-timeline:')
|
|
373
|
-
for (const e of terrs) console.error(` ${e}`)
|
|
374
|
-
return 2
|
|
375
|
-
}
|
|
376
|
-
const { axis } = normalizeTimeline(parsed)
|
|
377
|
-
const present = new Set(evidence.map((e) => AXIS_FOR_KIND[e.kind]))
|
|
378
|
-
if (!present.has(axis)) {
|
|
379
|
-
const have = evidence.map((e) => `${e.kind}→${AXIS_FOR_KIND[e.kind]}`).join(', ')
|
|
380
|
-
console.error(`spex eval add: --timeline axis '${axis}' matches none of this eval's evidence (${have}) — a step-map's axis must be the axis of an attached evidence entry`)
|
|
381
|
-
return 2
|
|
382
|
-
}
|
|
383
|
-
timelineBlob = putBlob(Buffer.from(JSON.stringify(parsed)))
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const reading: Reading = {
|
|
387
|
-
scenario: scenario.name,
|
|
388
|
-
codeSha: headSha(root),
|
|
389
|
-
// the contract this measurement was taken against — decides the scenario freshness axis by pure compare
|
|
390
|
-
scenarioHash: scenarioHash(scenario),
|
|
391
|
-
...(evidence.length ? { evidence } : {}),
|
|
392
|
-
...(timelineBlob ? { timelineBlob } : {}),
|
|
393
|
-
// the filing session — the originator an eval-comment thread loops in ([[mentions]]); absent if unknown
|
|
394
|
-
...((envSessionId() ?? undefined) ? { by: envSessionId()! } : {}),
|
|
395
|
-
verdict,
|
|
396
|
-
ts: new Date().toISOString(),
|
|
397
|
-
}
|
|
398
|
-
appendReading(node.sidecarPath, reading)
|
|
399
|
-
const ev = evidence.length
|
|
400
|
-
? evidence.map((e) => `${e.kind} ${e.hash.slice(0, 12)}…`).join(', ') + (timelineBlob ? ' +timeline' : '')
|
|
401
|
-
: 'no evidence'
|
|
402
|
-
console.log(` ✓ '${id}' scenario '${scenario.name}' → ${verdictText(verdict)} @ ${reading.codeSha.slice(0, 7)} (${ev})`)
|
|
403
|
-
console.log(`spex eval add: 1 measurement filed`)
|
|
404
|
-
|
|
405
|
-
// @@@mis-anchor guard - a codeSha names a COMMIT, never a working tree: filed over uncommitted governed
|
|
406
|
-
// edits, this reading claims a verdict at HEAD while HEAD lacks the code actually measured — wrong from
|
|
407
|
-
// birth, and the stale flag after the next commit is freshness exposing it, not an engine bug. Warn,
|
|
408
|
-
// never block: the honest flow is measure on the tree until green → commit that tested tree → file.
|
|
409
|
-
const dirty = dirtyGoverned(root, [...(scenario.code?.length ? scenario.code : node.codeFiles), node.evalPath])
|
|
410
|
-
if (dirty.length) {
|
|
411
|
-
console.error(` ⚠ mis-anchored eval: uncommitted changes in governed ${dirty.join(', ')}`)
|
|
412
|
-
console.error(` this eval anchors to HEAD ${reading.codeSha.slice(0, 7)}, which does NOT contain those edits — it claims a ${verdict.status} for code that never ran, and the commit that lands them will (correctly) read it stale.`)
|
|
413
|
-
console.error(` the honest flow: measure on the tree until green → commit that just-tested tree (code+spec) → THEN file the eval against the clean HEAD (retract this one if it recorded the dirty run).`)
|
|
414
|
-
}
|
|
415
|
-
return 0
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// the governed paths with uncommitted changes (staged, unstaged, or untracked) — the mis-anchor guard's
|
|
419
|
-
// probe. Paths are repo-relative pathspecs, so a governed dir prefix or `*` glob scopes the same way
|
|
420
|
-
// nodeChanged matches it; a rename line reports its new name.
|
|
421
|
-
function dirtyGoverned(root: string, paths: string[]): string[] {
|
|
422
|
-
if (!paths.length) return []
|
|
423
|
-
const out = git(['-C', root, 'status', '--porcelain', '--', ...paths])
|
|
424
|
-
const files = out.split('\n').filter(Boolean).map((l) => {
|
|
425
|
-
const p = l.slice(3)
|
|
426
|
-
const i = p.indexOf(' -> ')
|
|
427
|
-
return i >= 0 ? p.slice(i + 4) : p
|
|
428
|
-
})
|
|
429
|
-
return [...new Set(files)]
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// the verdict from the flags: --pass or --fail sets the status (pass wins if both given); --note <text> is an
|
|
433
|
-
// OPTIONAL annotation attached to either. No status flag → null (a measurement must commit to pass or fail).
|
|
434
|
-
function parseVerdict(args: string[]): Verdict | null {
|
|
435
|
-
const note = flag(args, 'note')
|
|
436
|
-
const ann = note !== undefined ? { note } : {}
|
|
437
|
-
if (has(args, 'pass')) return { status: 'pass', ...ann }
|
|
438
|
-
if (has(args, 'fail')) return { status: 'fail', ...ann }
|
|
439
|
-
return null
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// retract's flag set is closed like eval's — an unknown flag is rejected LOUD, never silently ignored.
|
|
443
|
-
const RETRACT_VALUE_FLAGS = new Set(['scenario', 'ts', 'note'])
|
|
444
|
-
const RETRACT_BOOL_FLAGS = new Set(['last'])
|
|
445
|
-
|
|
446
|
-
// `spex eval retract` — the sanctioned inverse of eval: undo a botched filing through the SAME surface
|
|
447
|
-
// that wrote it. It appends a RETRACTION event to the sidecar (append-only stays true; the target line
|
|
448
|
-
// stays as history; git carries who/when/why) — never a deleted line. The effective scoreboard then drops
|
|
449
|
-
// the retracted reading everywhere at once: the previous reading becomes the latest again, or the scenario
|
|
450
|
-
// honestly returns to eval-missing. Default target is the scenario's LATEST effective reading (--last is
|
|
451
|
-
// that default made explicit — repeated retracts peel junk e2e/smoke filings back one at a time); --ts
|
|
452
|
-
// pins an exact reading by its timestamp.
|
|
453
|
-
async function retractCmd(args: string[]): Promise<number> {
|
|
454
|
-
const root = repoRoot()
|
|
455
|
-
for (let i = 0; i < args.length; i++) {
|
|
456
|
-
const a = args[i]
|
|
457
|
-
if (!a.startsWith('--')) continue
|
|
458
|
-
const name = a.slice(2)
|
|
459
|
-
if (RETRACT_VALUE_FLAGS.has(name)) { i++; continue }
|
|
460
|
-
if (RETRACT_BOOL_FLAGS.has(name)) continue
|
|
461
|
-
console.error(`spex eval retract: unknown flag '${a}' — accepts --scenario --last --ts --note`)
|
|
462
|
-
return 2
|
|
463
|
-
}
|
|
464
|
-
const sel = positional(args)
|
|
465
|
-
const ref = !sel || sel === '.' ? currentNodeId(root) : stripRefSigil(sel)
|
|
466
|
-
if (!ref) { console.error('spex eval retract .: no current node (no .session/node-branch here) — name a node'); return 2 }
|
|
467
|
-
// node resolution mirrors eval's: exact canonical id, else a unique bare leaf, ambiguous fails loud.
|
|
468
|
-
const res = resolveEvalNode(evalNodes(root), ref)
|
|
469
|
-
if (!res.ok) { console.error(`spex eval retract: ${res.error}`); return 1 }
|
|
470
|
-
const node = res.node
|
|
471
|
-
const id = node.id
|
|
472
|
-
|
|
473
|
-
// which scenario, resolved exactly as eval resolves it: --scenario, or the sole declared scenario. A
|
|
474
|
-
// reading for a since-deleted scenario is still retractable by naming it — the sidecar knows names the
|
|
475
|
-
// eval.md may have dropped.
|
|
476
|
-
const scName = flag(args, 'scenario')
|
|
477
|
-
const declared = node.scenarios.map((s) => s.name)
|
|
478
|
-
const scenario = scName ?? (declared.length === 1 ? declared[0] : undefined)
|
|
479
|
-
if (!scenario) {
|
|
480
|
-
console.error(`spex eval retract: '${id}' declares ${declared.length} scenarios — name one with --scenario <name> (declared: ${declared.join(', ')})`)
|
|
481
|
-
return 1
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const ts = flag(args, 'ts')
|
|
485
|
-
if (ts !== undefined && has(args, 'last')) { console.error('spex eval retract: --ts and --last conflict — pin one eval or take the latest, not both'); return 2 }
|
|
486
|
-
const effective = readReadings(node.sidecarPath).filter((r) => r.scenario === scenario)
|
|
487
|
-
if (!effective.length) {
|
|
488
|
-
const { readings } = readSidecar(node.sidecarPath)
|
|
489
|
-
const had = readings.some((r) => r.scenario === scenario)
|
|
490
|
-
console.error(`spex eval retract: '${id}' scenario '${scenario}' has no ${had ? 'un-retracted ' : ''}eval${had ? ' left' : ''} — nothing to retract`)
|
|
491
|
-
return 1
|
|
492
|
-
}
|
|
493
|
-
const target = ts !== undefined ? effective.find((r) => r.ts === ts) : effective[effective.length - 1]
|
|
494
|
-
if (!target) {
|
|
495
|
-
console.error(`spex eval retract: '${id}' scenario '${scenario}' has no un-retracted eval @ ${ts} — evals: ${effective.map((r) => r.ts).join(', ')}`)
|
|
496
|
-
return 1
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const note = flag(args, 'note')
|
|
500
|
-
const retraction: Retraction = {
|
|
501
|
-
retracts: target.ts,
|
|
502
|
-
scenario,
|
|
503
|
-
...(note !== undefined ? { note } : {}),
|
|
504
|
-
...((envSessionId() ?? undefined) ? { by: envSessionId()! } : {}),
|
|
505
|
-
ts: new Date().toISOString(),
|
|
506
|
-
}
|
|
507
|
-
appendRetraction(node.sidecarPath, retraction)
|
|
508
|
-
const left = readReadings(node.sidecarPath).filter((r) => r.scenario === scenario)
|
|
509
|
-
const now = left.length
|
|
510
|
-
? `latest is now ${left[left.length - 1].ts} (${verdictText(left[left.length - 1].verdict)})`
|
|
511
|
-
: 'the scenario is unmeasured again (eval-missing)'
|
|
512
|
-
console.log(` ⟲ '${id}' scenario '${scenario}' eval @ ${target.ts} (${verdictText(target.verdict)}) retracted — ${now}`)
|
|
513
|
-
console.log('spex eval retract: 1 eval retracted (an appended event — commit the sidecar so the retraction is attributed)')
|
|
514
|
-
return 0
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// `spex eval ok` — the HUMAN sign-off on a scenario's latest reading ([[human-ok]]): appends a monotonic
|
|
518
|
-
// human-ok event bound to that one immutable reading (a newer reading or live-computed staleness brings the
|
|
519
|
-
// scenario back on its own — no un-ok verb exists). CLI parity with the dashboard's affordance (LAW L);
|
|
520
|
-
// identity is the surface's: 'human' here — and a GOVERNED session is REFUSED, because the sign-off is the
|
|
521
|
-
// human's own deliberate judgment and an agent blessing its own reading would hide it from exactly the
|
|
522
|
-
// review the ok certifies (the no-self-resolve analogue). Flag set closed, like every eval verb.
|
|
523
|
-
async function okCmd(args: string[]): Promise<number> {
|
|
524
|
-
const root = repoRoot()
|
|
525
|
-
for (let i = 0; i < args.length; i++) {
|
|
526
|
-
const a = args[i]
|
|
527
|
-
if (!a.startsWith('--')) continue
|
|
528
|
-
if (a === '--scenario') { i++; continue }
|
|
529
|
-
console.error(`spex eval ok: unknown flag '${a}' — accepts --scenario`)
|
|
530
|
-
return 2
|
|
531
|
-
}
|
|
532
|
-
if (envSessionId()) {
|
|
533
|
-
console.error('spex eval ok: refusing under a governed session — human-ok is the HUMAN\'s sign-off, never an agent\'s self-blessing. Ask the human to ok it from the dashboard or their own terminal; an agent\'s judgment on an eval is a remark (`spex remark add`).')
|
|
534
|
-
return 1
|
|
535
|
-
}
|
|
536
|
-
const sel = positional(args)
|
|
537
|
-
if (!sel || sel === '.') { console.error('spex eval ok: name a node — usage: spex eval ok <node> --scenario <name>'); return 2 }
|
|
538
|
-
const node = stripRefSigil(sel)
|
|
539
|
-
const scName = flag(args, 'scenario')
|
|
540
|
-
const res = resolveEvalNode(evalNodes(root), node)
|
|
541
|
-
if (!res.ok) { console.error(`spex eval ok: ${res.error}`); return 1 }
|
|
542
|
-
const declared = res.node.scenarios.map((s) => s.name)
|
|
543
|
-
const scenario = scName ?? (declared.length === 1 ? declared[0] : undefined)
|
|
544
|
-
if (!scenario) {
|
|
545
|
-
console.error(`spex eval ok: '${res.node.id}' declares ${declared.length} scenarios — name one with --scenario <name> (declared: ${declared.join(', ')})`)
|
|
546
|
-
return 1
|
|
547
|
-
}
|
|
548
|
-
const { fileHumanOk } = await import('./humanok.js')
|
|
549
|
-
const r = fileHumanOk(res.node.id, scenario, 'human')
|
|
550
|
-
if (!r.ok) { console.error(`spex eval ok: ${r.error}`); return 1 }
|
|
551
|
-
if (r.already) {
|
|
552
|
-
console.log(`spex eval ok: '${res.node.id}' scenario '${scenario}' eval @ ${r.humanOk.okTs} is already human-ok'd (by ${r.humanOk.by}, ${r.humanOk.ts}) — monotonic, nothing appended`)
|
|
553
|
-
return 0
|
|
554
|
-
}
|
|
555
|
-
console.log(` ☑ '${res.node.id}' scenario '${scenario}' eval @ ${r.humanOk.okTs} (${r.humanOk.okSha.slice(0, 7)}) human-ok'd`)
|
|
556
|
-
console.log(r.landed === 'committed'
|
|
557
|
-
? 'spex eval ok: 1 sign-off filed (committed straight to the trunk)'
|
|
558
|
-
: 'spex eval ok: 1 sign-off filed (an appended event — commit the sidecar so the sign-off lands)')
|
|
559
|
-
return 0
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
async function clean(args: string[]): Promise<number> {
|
|
563
|
-
const root = repoRoot()
|
|
564
|
-
const all = has(args, 'all')
|
|
565
|
-
const keepLatest = has(args, 'keep-latest')
|
|
566
|
-
const referenced = new Set<string>()
|
|
567
|
-
if (!all) {
|
|
568
|
-
for (const n of await gatherNodes(root)) {
|
|
569
|
-
const readings = readReadings(n.sidecarPath)
|
|
570
|
-
const keep = keepLatest ? [...latestPerScenario(readings).values()] : readings
|
|
571
|
-
for (const r of keep) {
|
|
572
|
-
for (const e of evidenceOf(r)) referenced.add(e.hash) // every evidence entry (N images + a video…)
|
|
573
|
-
if (r.timelineBlob) referenced.add(r.timelineBlob) // a video reading's step map lives in the same cache
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
const before = listBlobs().length
|
|
578
|
-
const removed = gc(referenced)
|
|
579
|
-
const mode = all ? 'all' : keepLatest ? 'keep-latest' : 'unreferenced'
|
|
580
|
-
console.log(`spex eval clean: removed ${removed.length} evidence file(s), kept ${before - removed.length} (${mode})`)
|
|
581
|
-
return 0
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function checkStaged(): number {
|
|
585
|
-
const root = repoRoot()
|
|
586
|
-
const tagLibrary = loadConfig(root).scenarioTags
|
|
587
|
-
const staged = stagedFiles(root)
|
|
588
|
-
let bad = false
|
|
589
|
-
|
|
590
|
-
const blobs = staged.filter(isStrayBlob)
|
|
591
|
-
if (blobs.length) {
|
|
592
|
-
bad = true
|
|
593
|
-
console.error('✗ SpexCode eval: stray evidence file(s) staged — evidence lives in the shared git common dir, never in the tree:')
|
|
594
|
-
for (const o of blobs) console.error(` ${o}`)
|
|
595
|
-
console.error(' Unstage them (git rm --cached <path>); an eval references its evidence by hash, it never commits the bytes.')
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
for (const rel of staged.filter((p) => p === EVAL_FILE || p.endsWith('/' + EVAL_FILE))) {
|
|
599
|
-
const abs = join(root, rel)
|
|
600
|
-
if (!existsSync(abs)) continue // staged deletion — nothing to validate
|
|
601
|
-
const errs = validateScenarios(readFileSync(abs, 'utf8'), tagLibrary, root)
|
|
602
|
-
if (!errs.length) continue
|
|
603
|
-
bad = true
|
|
604
|
-
console.error(`✗ SpexCode eval: ${rel} — invalid scenario schema:`)
|
|
605
|
-
for (const e of errs) console.error(` ${e}`)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
return bad ? 1 : 0
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async function show(args: string[]): Promise<number> {
|
|
612
|
-
const root = repoRoot()
|
|
613
|
-
const sel = positional(args)
|
|
614
|
-
const ref = !sel || sel === '.' ? currentNodeId(root) : stripRefSigil(sel)
|
|
615
|
-
if (!ref) { console.error('spex eval ls .: no current node (no .session/node-branch here) — name a node'); return 2 }
|
|
616
|
-
// resolve LOUD before the timeline: an ambiguous bare leaf must list its candidate canonical ids, never
|
|
617
|
-
// fall through to a false "declares no scenarios". A ref matching NO measurable node still renders the honest
|
|
618
|
-
// hasEvalFile:false line — a spec node without an eval.md is not an error to look at.
|
|
619
|
-
const res = resolveEvalNode(evalNodes(root), ref)
|
|
620
|
-
if (!res.ok && res.ambiguous) { console.error(`spex eval ls: ${res.error}`); return 1 }
|
|
621
|
-
const tl = await evalTimeline(res.ok ? res.node.id : ref) // no ctx → evalTimeline derives specs + driftIndex itself for this one id
|
|
622
|
-
if (has(args, 'json')) { console.log(JSON.stringify(tl, null, 2)); return 0 }
|
|
623
|
-
console.log(formatTimeline(tl))
|
|
624
|
-
return 0
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// the verdict as a short tag for the terminal: ✓ pass / ✗ fail, with ` — <note>` appended when annotated;
|
|
628
|
-
// `legacy` for a reading taken before verdicts existed, `≈ <note>` for a legacy note-only reading.
|
|
629
|
-
function verdictText(v: Verdict | undefined): string {
|
|
630
|
-
if (!v) return 'legacy'
|
|
631
|
-
if (v.status === 'pass') return v.note ? `✓ pass — ${v.note}` : '✓ pass'
|
|
632
|
-
if (v.status === 'fail') return v.note ? `✗ fail — ${v.note}` : '✗ fail'
|
|
633
|
-
return v.note ? `≈ ${v.note}` : 'legacy' // legacy note-only reading
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
export function formatTimeline(tl: EvalTimeline): string {
|
|
637
|
-
if (!tl.hasEvalFile) return `spex eval ls: '${tl.node}' declares no scenarios (no eval.md)`
|
|
638
|
-
// the retraction trace, newest first — the undo stays visible through the same surface that shows readings.
|
|
639
|
-
const retractLines = (tl.retractions ?? []).map((x) =>
|
|
640
|
-
` ⟲ retracted: scenario '${x.scenario}' eval @ ${x.retracts}${x.note ? ` — ${x.note}` : ''}${x.by ? ` by ${x.by}` : ''} ${x.ts}`)
|
|
641
|
-
if (!tl.readings.length) {
|
|
642
|
-
const head = `spex eval ls: '${tl.node}' has scenarios but no eval yet — run \`spex eval add ${tl.node}\``
|
|
643
|
-
return retractLines.length ? [head, '', ...retractLines].join('\n') : head
|
|
644
|
-
}
|
|
645
|
-
const w = Math.max(...tl.readings.map((r) => r.scenario.length))
|
|
646
|
-
const lines = tl.readings.flatMap((r) => {
|
|
647
|
-
const badge = r.fresh ? '✓ current' : `⚠ stale (${r.staleAxes.join(', ')})`
|
|
648
|
-
// the reading's whole evidence list (N images + a video…), each cell its kind + short hash, or the honest
|
|
649
|
-
// sentinel for a pruned/absent one; falls back to the scalar view for a legacy/test EvalEntry.
|
|
650
|
-
const list = r.evidence?.length ? r.evidence
|
|
651
|
-
: r.blob != null ? [{ hash: r.blob, kind: r.blobKind ?? 'image', state: r.blobState }] : []
|
|
652
|
-
const ev = list.length
|
|
653
|
-
? list.map((e) => e.state === 'miss' ? 'miss original file' : `${e.kind} ${(e.hash ?? '').slice(0, 12)}…`).join(', ')
|
|
654
|
-
: 'no evidence'
|
|
655
|
-
const okTag = r.humanOk ? ` ☑ human-ok (${r.humanOk.by})` : ''
|
|
656
|
-
const head = ` ${r.scenario.padEnd(w)} ${verdictText(r.verdict)} ${badge}${okTag} ${r.codeSha.slice(0, 7)} ${ev} ${r.ts}`
|
|
657
|
-
return r.expected ? [head, ` ${' '.repeat(w)} expected: ${r.expected}`] : [head]
|
|
658
|
-
})
|
|
659
|
-
return [`spex eval ls: '${tl.node}' — ${tl.readings.length} eval(s), newest first`, '', ...lines, ...retractLines].join('\n')
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// `spex eval scenario ls [<node>|.] [--unmeasured] [--json]` — the DECLARED half of the scoreboard: list
|
|
663
|
-
// scenarios (the measurement contracts) rather than readings. Bare = every measurable node's scenarios
|
|
664
|
-
// (the collection view); a <node>/`.` scopes to one node. --unmeasured keeps only scenarios with NO
|
|
665
|
-
// effective reading — never measured, or every filing retracted — the blind-spot worklist a measuring
|
|
666
|
-
// hand picks from. Flag set closed, like every eval verb.
|
|
667
|
-
export type ScenarioListRow = {
|
|
668
|
-
node: string
|
|
669
|
-
scenario: string
|
|
670
|
-
tags?: string[]
|
|
671
|
-
test?: ScenarioTestReference
|
|
672
|
-
measured: boolean
|
|
673
|
-
latest?: { verdict?: Verdict; ts: string }
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
export function scenarioListRows(nodes: EvalNode[], unmeasuredOnly = false): ScenarioListRow[] {
|
|
677
|
-
const rows: ScenarioListRow[] = []
|
|
678
|
-
for (const n of nodes) {
|
|
679
|
-
const latest = latestPerScenario(readReadings(n.sidecarPath))
|
|
680
|
-
for (const sc of n.scenarios) {
|
|
681
|
-
const r = latest.get(sc.name)
|
|
682
|
-
if (unmeasuredOnly && r) continue
|
|
683
|
-
rows.push({
|
|
684
|
-
node: n.id, scenario: sc.name,
|
|
685
|
-
...(sc.tags?.length ? { tags: sc.tags } : {}),
|
|
686
|
-
...(sc.test ? { test: sc.test } : {}),
|
|
687
|
-
measured: !!r,
|
|
688
|
-
...(r ? { latest: { ...(r.verdict ? { verdict: r.verdict } : {}), ts: r.ts } } : {}),
|
|
689
|
-
})
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return rows
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function scenarioProjectionProvenance(root: string): { head: string; treeSha: string } {
|
|
696
|
-
// These are fixed-tree provenance fields, deliberately outside semanticIndexHash/fullIndexHash. A mode or
|
|
697
|
-
// type change therefore remains visible to a tree consumer without changing the content-derived scenario
|
|
698
|
-
// identities. The command runs inside a Git project, so failure is loud instead of manufacturing provenance.
|
|
699
|
-
const head = git(['-C', root, 'rev-parse', 'HEAD']).trim()
|
|
700
|
-
if (!head) throw new Error('cannot resolve one fixed HEAD provenance')
|
|
701
|
-
const treeSha = git(['-C', root, 'rev-parse', `${head}^{tree}`]).trim()
|
|
702
|
-
if (!treeSha) throw new Error(`cannot resolve tree provenance for fixed HEAD ${head}`)
|
|
703
|
-
return { head, treeSha }
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function scenarioWrite(args: string[]): number {
|
|
707
|
-
let rawMutation: string | undefined
|
|
708
|
-
for (let i = 0; i < args.length; i++) {
|
|
709
|
-
const arg = args[i]
|
|
710
|
-
if (arg !== '--mutation') {
|
|
711
|
-
console.error(`spex eval scenario write: unknown argument '${arg}' — accepts only --mutation <json>; eval.md bytes come from stdin`)
|
|
712
|
-
return 2
|
|
713
|
-
}
|
|
714
|
-
if (rawMutation !== undefined) {
|
|
715
|
-
console.error('spex eval scenario write: --mutation must be supplied exactly once')
|
|
716
|
-
return 2
|
|
717
|
-
}
|
|
718
|
-
const value = args[++i]
|
|
719
|
-
if (value === undefined) {
|
|
720
|
-
console.error('spex eval scenario write: --mutation needs one JSON value')
|
|
721
|
-
return 2
|
|
722
|
-
}
|
|
723
|
-
rawMutation = value
|
|
724
|
-
}
|
|
725
|
-
if (rawMutation === undefined) {
|
|
726
|
-
console.error('spex eval scenario write: usage: spex eval scenario write --mutation <json> < eval.md')
|
|
727
|
-
return 2
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
const mutation = JSON.parse(rawMutation)
|
|
732
|
-
const source = readFileSync(0, 'utf8')
|
|
733
|
-
process.stdout.write(writeScenarioMeasurementMetadata(source, mutation))
|
|
734
|
-
return 0
|
|
735
|
-
} catch (e: any) {
|
|
736
|
-
console.error(`spex eval scenario write: ${e?.message ?? e}`)
|
|
737
|
-
return 1
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
async function scenarioLs(args: string[]): Promise<number> {
|
|
742
|
-
for (const a of args) {
|
|
743
|
-
if (!a.startsWith('--')) continue
|
|
744
|
-
const name = a.slice(2)
|
|
745
|
-
if (name !== 'unmeasured' && name !== 'json') {
|
|
746
|
-
console.error(`spex eval scenario ls: unknown flag '${a}' — accepts --unmeasured --json`)
|
|
747
|
-
return 2
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
const root = repoRoot()
|
|
751
|
-
const sel = positional(args)
|
|
752
|
-
const unmeasuredOnly = has(args, 'unmeasured')
|
|
753
|
-
if (has(args, 'json')) {
|
|
754
|
-
if (unmeasuredOnly) {
|
|
755
|
-
console.error('spex eval scenario ls --json: --unmeasured needs evals; JSON is the complete declaration projection and reads no eval sidecar')
|
|
756
|
-
return 2
|
|
757
|
-
}
|
|
758
|
-
try {
|
|
759
|
-
const before = scenarioProjectionProvenance(root)
|
|
760
|
-
// Read declarations from the exact commit named by `before`, never from a dirty/staged working tree.
|
|
761
|
-
const fixedNodes = evalNodesAt(root, before.head)
|
|
762
|
-
const selected = sel
|
|
763
|
-
? (() => {
|
|
764
|
-
const ref = sel === '.' ? currentNodeId(root) : stripRefSigil(sel)
|
|
765
|
-
if (!ref) throw new Error('no current node (no session/node-branch here) — name a node')
|
|
766
|
-
const res = resolveEvalNode(fixedNodes, ref)
|
|
767
|
-
if (!res.ok) throw new Error(res.error)
|
|
768
|
-
return [res.node]
|
|
769
|
-
})()
|
|
770
|
-
: fixedNodes
|
|
771
|
-
console.log(JSON.stringify(scenarioProjection(selected, before), null, 2))
|
|
772
|
-
return 0
|
|
773
|
-
} catch (e: any) {
|
|
774
|
-
console.error(`spex eval scenario ls --json: ${e?.message ?? e}`)
|
|
775
|
-
return 1
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
let nodes = evalNodes(root)
|
|
779
|
-
if (sel) {
|
|
780
|
-
const ref = sel === '.' ? currentNodeId(root) : stripRefSigil(sel)
|
|
781
|
-
if (!ref) { console.error('spex eval scenario ls .: no current node (no session/node-branch here) — name a node'); return 2 }
|
|
782
|
-
const res = resolveEvalNode(nodes, ref)
|
|
783
|
-
if (!res.ok) { console.error(`spex eval scenario ls: ${res.error}`); return 1 }
|
|
784
|
-
nodes = [res.node]
|
|
785
|
-
}
|
|
786
|
-
const rows = scenarioListRows(nodes, unmeasuredOnly)
|
|
787
|
-
if (!rows.length) {
|
|
788
|
-
console.log(`spex eval scenario ls: ${unmeasuredOnly ? 'no unmeasured scenarios' : 'no scenarios declared'}${sel ? ` on '${nodes[0]?.id ?? sel}'` : ''}`)
|
|
789
|
-
return 0
|
|
790
|
-
}
|
|
791
|
-
const w = Math.max(...rows.map((r) => r.node.length))
|
|
792
|
-
for (const r of rows) {
|
|
793
|
-
const tagStr = r.tags?.length ? ` [${r.tags.join(',')}]` : ''
|
|
794
|
-
const state = r.measured ? `${verdictText(r.latest!.verdict)} ${r.latest!.ts}` : '∅ unmeasured'
|
|
795
|
-
console.log(` ${r.node.padEnd(w)} ${r.scenario}${tagStr} — ${state}`)
|
|
796
|
-
}
|
|
797
|
-
const un = rows.filter((r) => !r.measured).length
|
|
798
|
-
console.log(`spex eval scenario ls: ${rows.length} scenario(s)${unmeasuredOnly ? ', all unmeasured' : un ? ` (${un} unmeasured)` : ''}`)
|
|
799
|
-
return 0
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// the `spex eval` drawer's node-scoped verbs ([[cli-surface]]): add (file a measurement) · ls (a node's
|
|
803
|
-
// reading timeline) · scenario ls/write (the declared contracts and their canonical metadata writer) · lint (the
|
|
804
|
-
// measurement-layer lint — findings are advisory; an unreadable --changed scope fails) · ok (the human
|
|
805
|
-
// sign-off) · retract · clean.
|
|
806
|
-
// The session-scoped read (`spex eval ls --session <SEL>`) is intercepted in cli.ts before this runs;
|
|
807
|
-
// `check-staged` is hook plumbing, exported separately for `spex internal check-staged`.
|
|
808
|
-
export async function runEval(args: string[]): Promise<number> {
|
|
809
|
-
const sub = args[0]
|
|
810
|
-
if (sub === 'lint') return scan(args.slice(1))
|
|
811
|
-
if (sub === 'add') return evalCmd(args.slice(1))
|
|
812
|
-
if (sub === 'ok') return okCmd(args.slice(1))
|
|
813
|
-
if (sub === 'retract') return retractCmd(args.slice(1))
|
|
814
|
-
if (sub === 'clean') return clean(args.slice(1))
|
|
815
|
-
if (sub === 'ls') return show(args.slice(1))
|
|
816
|
-
if (sub === 'scenario') {
|
|
817
|
-
if (args[1] === 'ls') return scenarioLs(args.slice(2))
|
|
818
|
-
if (args[1] === 'write') return scenarioWrite(args.slice(2))
|
|
819
|
-
console.error('spex eval scenario: ls [<node>|.] [--unmeasured] [--json] | write --mutation <json> < eval.md')
|
|
820
|
-
return 2
|
|
821
|
-
}
|
|
822
|
-
console.error('spex eval: add [.|<node>] [--scenario <name>] (--pass|--fail) [--note <text>] [--image <path> …repeatable] [--result <path|->] [--video <path>] [--timeline <json>] | ls [.|<node>] [--json] | ls --session <SEL> [--export] | scenario ls [<node>|.] [--unmeasured] [--json] | scenario write --mutation <json> < eval.md | lint [--changed] | ok <node> [--scenario <name>] | retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>] | clean [--keep-latest|--all]')
|
|
823
|
-
return 2
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
export { checkStaged }
|
|
827
|
-
|
|
828
|
-
// `spex evidence put <file|->` / `spex evidence get <hash> [-o <file>]` ([[evidence-put]], [[evidence-get]]) — the bare
|
|
829
|
-
// evidence-transport pair: put stashes bytes in the shared content-addressed cache and prints the hash,
|
|
830
|
-
// WITHOUT filing a reading (`eval add --video` couples the two); get is its symmetric read — hash in,
|
|
831
|
-
// bytes out. putBlob is idempotent by content, so re-putting re-seeds a checkout whose cache lacks a blob
|
|
832
|
-
// some thread already references by hash (the clone-evidence-404 repair).
|
|
833
|
-
export async function runEvidence(args: string[]): Promise<number> {
|
|
834
|
-
if (args[0] === 'put' && args[1] !== undefined) return blobPut(args[1])
|
|
835
|
-
if (args[0] === 'get') return blobGet(args.slice(1))
|
|
836
|
-
console.error('spex evidence: put <file|-> — stash bytes in the shared evidence cache, print the content hash')
|
|
837
|
-
console.error(' get <hash> [-o <file>] — read evidence back: local cache first, backend fallback')
|
|
838
|
-
return 2
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function blobPut(file: string): number {
|
|
842
|
-
let bytes: Buffer
|
|
843
|
-
try { bytes = readFileSync(file === '-' ? 0 : file) } catch (e) {
|
|
844
|
-
console.error(`spex evidence put: cannot read ${file}: ${(e as Error).message}`)
|
|
845
|
-
return 2
|
|
846
|
-
}
|
|
847
|
-
if (bytes.length === 0) { console.error('spex evidence put: refusing empty evidence'); return 2 }
|
|
848
|
-
console.log(putBlob(bytes))
|
|
849
|
-
return 0
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// the read half: ① the local content-addressed cache (the evidence usually IS on this disk — no backend
|
|
853
|
-
// needed), ② on a local miss the same GET /api/evidence/:hash the dashboard streams from (the blob may
|
|
854
|
-
// have been pruned here, or put on another machine sharing the backend), ③ both missed → fail loud naming
|
|
855
|
-
// both paths. No third read mechanism — this reuses readBlobByHash and the existing endpoint verbatim.
|
|
856
|
-
async function blobGet(args: string[]): Promise<number> {
|
|
857
|
-
const oIdx = args.indexOf('-o')
|
|
858
|
-
const out = oIdx >= 0 ? args[oIdx + 1] : undefined
|
|
859
|
-
if (oIdx >= 0 && (out === undefined || out.startsWith('-'))) { console.error('spex evidence get: -o needs a <file>'); return 2 }
|
|
860
|
-
const hash = args.find((a, i) => (oIdx < 0 || (i !== oIdx && i !== oIdx + 1)) && !a.startsWith('-'))
|
|
861
|
-
if (!hash) { console.error('spex evidence get: usage: spex evidence get <hash> [-o <file>]'); return 2 }
|
|
862
|
-
const local = readBlobByHash(hash) // validates 64-hex before touching the fs, then reads the shared cache
|
|
863
|
-
if (local.ok) return emitBlob(local.bytes, out)
|
|
864
|
-
if (local.reason === 'invalid') { console.error(`spex evidence get: bad hash '${hash}' — an evidence hash is 64 hex chars`); return 2 }
|
|
865
|
-
const { apiBase } = await import('../../spec-cli/src/sessions.js')
|
|
866
|
-
const url = `${await apiBase()}/api/evidence/${hash}`
|
|
867
|
-
let backendMiss: string
|
|
868
|
-
try {
|
|
869
|
-
const r = await fetch(url)
|
|
870
|
-
if (r.ok) return emitBlob(Buffer.from(await r.arrayBuffer()), out)
|
|
871
|
-
backendMiss = `HTTP ${r.status}`
|
|
872
|
-
} catch (e) {
|
|
873
|
-
backendMiss = `unreachable (${(e as Error).message})`
|
|
874
|
-
}
|
|
875
|
-
console.error(`spex evidence get: ${hash} — not found on either path:`)
|
|
876
|
-
console.error(` local cache: ${blobPath(hash)} — no such evidence (pruned, or put on another machine)`)
|
|
877
|
-
console.error(` backend: ${url} — ${backendMiss}`)
|
|
878
|
-
return 1
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// default stdout (pipe-friendly; the cli.ts flushExit drains a large piped dump before exit), -o writes a
|
|
882
|
-
// file. Raw bytes straight at a human's terminal get a one-line stderr warning, not a block.
|
|
883
|
-
function emitBlob(bytes: Buffer, out?: string): number {
|
|
884
|
-
if (out !== undefined) {
|
|
885
|
-
try { writeFileSync(out, bytes) } catch (e) {
|
|
886
|
-
console.error(`spex evidence get: cannot write ${out}: ${(e as Error).message}`)
|
|
887
|
-
return 2
|
|
888
|
-
}
|
|
889
|
-
return 0
|
|
890
|
-
}
|
|
891
|
-
if (process.stdout.isTTY) console.error(`spex evidence get: writing ${bytes.length} raw bytes to a tty — pipe it or use -o <file>`)
|
|
892
|
-
process.stdout.write(bytes)
|
|
893
|
-
return 0
|
|
894
|
-
}
|