spexcode 0.6.2 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/spex.mjs +4 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
- package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
- package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
- package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +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 +325 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
- package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
- package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
- package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
- package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
- package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
- package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
- package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
- package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
- package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
- package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
- package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
- package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
- package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
- package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
- package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
- package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
- package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
- package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +1 -1
- package/node_modules/@spexcode/spec-cli/package.json +47 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/{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 +166 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
- package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
- package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
- package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
- package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
- package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
- package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
- package/node_modules/@spexcode/spec-core/package.json +28 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
- package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
- package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
- package/node_modules/@spexcode/spec-eval/package.json +36 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
- package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
- package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
- package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
- package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
- package/node_modules/@spexcode/spec-forge/package.json +32 -0
- package/package.json +33 -19
- package/spec-cli/bin/spex.mjs +0 -69
- package/spec-cli/src/anchors.ts +0 -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/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
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { HARNESSES, MISSING_DEFAULT_LAUNCHER_ERROR, defaultLauncher, harnessById, launcherList, resolveLauncher } from './harness.js';
|
|
8
|
+
import { ensureDashboardArtifact } from './dashboard-assets.js';
|
|
9
|
+
const PKG = fileURLToPath(new URL('..', import.meta.url));
|
|
10
|
+
const SPEX = join(PKG, 'bin', 'spex.mjs');
|
|
11
|
+
// The branch Flatcode commits the spec tree onto inside its OWN clone. Never pushed: the flat is a reading of
|
|
12
|
+
// the repository, not a change to it, and a user who wants it upstream opens that as their own change.
|
|
13
|
+
export const FLAT_BRANCH = 'flatcode';
|
|
14
|
+
const DEFAULT_ROUNDS = 6;
|
|
15
|
+
const DEFAULT_COVERAGE = 90;
|
|
16
|
+
// A Flatcode run begins outside the target repository, so an installed CLI cannot rely on that directory
|
|
17
|
+
// already having SpexCode launcher profiles. These are the regular local agent commands an operator can select
|
|
18
|
+
// at the boundary; an existing named launcher still wins and preserves its configured command.
|
|
19
|
+
export const FLAT_AGENT_CHOICES = [
|
|
20
|
+
{ name: 'claude', label: 'Claude Code', harness: 'claude', cmd: 'claude' },
|
|
21
|
+
{ name: 'codex', label: 'Codex', harness: 'codex', cmd: 'codex' },
|
|
22
|
+
{ name: 'opencode', label: 'OpenCode', harness: 'opencode', cmd: 'opencode' },
|
|
23
|
+
{ name: 'pi', label: 'Pi', harness: 'pi', cmd: 'pi' },
|
|
24
|
+
];
|
|
25
|
+
// @@@ prose language - a spec is written FOR the people who maintain the repository, and for most of the
|
|
26
|
+
// world that is not English. The language governs the prose only: node ids are directory names, which
|
|
27
|
+
// [[id-url-safe]] holds to a URL-safe ASCII form, so a tree written in Chinese still has ascii-kebab ids.
|
|
28
|
+
// Unlisted codes pass through verbatim, so `--lang "Brazilian Portuguese"` works without a table entry.
|
|
29
|
+
const LANGUAGE_NAMES = {
|
|
30
|
+
zh: '简体中文 (Simplified Chinese)', 'zh-cn': '简体中文 (Simplified Chinese)',
|
|
31
|
+
'zh-tw': '繁體中文 (Traditional Chinese)', en: 'English', ja: '日本語 (Japanese)',
|
|
32
|
+
ko: '한국어 (Korean)', fr: 'French', de: 'German', es: 'Spanish', pt: 'Portuguese', ru: 'Russian',
|
|
33
|
+
};
|
|
34
|
+
export const languageName = (code) => LANGUAGE_NAMES[code.toLowerCase()] ?? code;
|
|
35
|
+
// @@@ source extensions are an allowlist, never a guess - an unknown extension is ignored rather than
|
|
36
|
+
// governed. Governing a file nobody can spec (a lockfile, a minified vendor bundle, a fixture) manufactures
|
|
37
|
+
// permanent coverage debt that no round can ever pay off, so the gate would never close. Under-claiming costs
|
|
38
|
+
// a user one edit to the emitted config; over-claiming costs an unbounded loop.
|
|
39
|
+
const SOURCE_EXTENSIONS = {
|
|
40
|
+
ts: 'TypeScript', tsx: 'TypeScript', js: 'JavaScript', jsx: 'JavaScript', mjs: 'JavaScript',
|
|
41
|
+
py: 'Python', rb: 'Ruby', go: 'Go', rs: 'Rust', java: 'Java', kt: 'Kotlin', swift: 'Swift',
|
|
42
|
+
c: 'C', h: 'C', cc: 'C++', cpp: 'C++', hpp: 'C++', cs: 'C#', php: 'PHP', scala: 'Scala',
|
|
43
|
+
ex: 'Elixir', exs: 'Elixir', erl: 'Erlang', hs: 'Haskell', ml: 'OCaml', clj: 'Clojure',
|
|
44
|
+
lua: 'Lua', dart: 'Dart', vue: 'Vue', svelte: 'Svelte', sh: 'Shell', zig: 'Zig',
|
|
45
|
+
};
|
|
46
|
+
// Tracked directories that are somebody else's code or a build product. They are real source by extension,
|
|
47
|
+
// which is exactly why they must be named: governing them buys nothing and drowns the real tree.
|
|
48
|
+
const NON_SOURCE_DIRS = new Set([
|
|
49
|
+
'node_modules', 'vendor', 'third_party', 'thirdparty', 'dist', 'build', 'out', 'target',
|
|
50
|
+
'generated', 'gen', '.venv', 'venv', 'site-packages', 'testdata', 'fixtures', '__pycache__',
|
|
51
|
+
// SpexCode's own tree is never the subject of a flat. It is absent on the first pass (profiling runs before
|
|
52
|
+
// the seed) but present on any re-profile, where counting it would shift the share threshold under roots
|
|
53
|
+
// that qualified the first time.
|
|
54
|
+
'.spec',
|
|
55
|
+
]);
|
|
56
|
+
function run(command, args, cwd, stdin = '') {
|
|
57
|
+
return new Promise((done) => {
|
|
58
|
+
const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
59
|
+
let stdout = '';
|
|
60
|
+
let stderr = '';
|
|
61
|
+
child.stdout.setEncoding('utf8');
|
|
62
|
+
child.stderr.setEncoding('utf8');
|
|
63
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
64
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
65
|
+
child.on('error', (error) => done({ code: 127, stdout, stderr: `${stderr}${error.message}` }));
|
|
66
|
+
child.on('close', (code) => done({ code: code ?? 1, stdout, stderr }));
|
|
67
|
+
child.stdin.on('error', () => { });
|
|
68
|
+
child.stdin.end(stdin);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// One agent turn, streamed to the operator's terminal as it happens. A conversion round takes minutes; a
|
|
72
|
+
// silent pipe would read as a hang, and the turn's own narration is the only progress there is to show.
|
|
73
|
+
function runTurn(turn, cwd) {
|
|
74
|
+
return new Promise((done) => {
|
|
75
|
+
const child = spawn('sh', ['-c', turn.command], { cwd, stdio: ['pipe', 'inherit', 'inherit'] });
|
|
76
|
+
child.on('error', () => done(127));
|
|
77
|
+
child.on('close', (code) => done(code ?? 1));
|
|
78
|
+
// @@@ the exit code is the verdict, not the pipe - a harness that takes the prompt and closes stdin, or
|
|
79
|
+
// that exits before reading it at all (a rejected credential, a bad flag), makes this write EPIPE. That is
|
|
80
|
+
// information about the pipe, not about the round: an unhandled EPIPE here would abort the whole flat and
|
|
81
|
+
// throw away every converged round before it, where the exit code turns the same event into one reported
|
|
82
|
+
// failed round the loop can measure and continue past.
|
|
83
|
+
child.stdin.on('error', () => { });
|
|
84
|
+
child.stdin.end(turn.stdin);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const spex = (args, cwd, env = {}) => new Promise((done) => {
|
|
88
|
+
const child = spawn(process.execPath, [SPEX, ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env } });
|
|
89
|
+
let stdout = '';
|
|
90
|
+
let stderr = '';
|
|
91
|
+
child.stdout.setEncoding('utf8');
|
|
92
|
+
child.stderr.setEncoding('utf8');
|
|
93
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
94
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
95
|
+
child.on('error', (error) => done({ code: 127, stdout, stderr: error.message }));
|
|
96
|
+
child.on('close', (code) => done({ code: code ?? 1, stdout, stderr }));
|
|
97
|
+
});
|
|
98
|
+
// @@@ profile - derive what counts as this repository's source from what it actually tracks. `git ls-files`
|
|
99
|
+
// is the enumeration because it already answers "tracked, not ignored" exactly; walking the tree would have to
|
|
100
|
+
// re-implement .gitignore and would sweep in build output the repository deliberately ignores.
|
|
101
|
+
export function profileFiles(files) {
|
|
102
|
+
const byExtension = new Map();
|
|
103
|
+
const byRoot = new Map();
|
|
104
|
+
let fileCount = 0;
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const segments = file.split('/');
|
|
107
|
+
if (segments.some((segment) => NON_SOURCE_DIRS.has(segment)))
|
|
108
|
+
continue;
|
|
109
|
+
const leaf = segments[segments.length - 1];
|
|
110
|
+
const dot = leaf.lastIndexOf('.');
|
|
111
|
+
if (dot <= 0)
|
|
112
|
+
continue; // no extension at all, or a dotfile (.gitignore)
|
|
113
|
+
const extension = leaf.slice(dot + 1).toLowerCase();
|
|
114
|
+
if (!(extension in SOURCE_EXTENSIONS))
|
|
115
|
+
continue;
|
|
116
|
+
fileCount += 1;
|
|
117
|
+
byExtension.set(extension, (byExtension.get(extension) ?? 0) + 1);
|
|
118
|
+
// A file at the repository top level makes the repository root the governed root; there is no dir to name.
|
|
119
|
+
const root = segments.length > 1 ? segments[0] : '.';
|
|
120
|
+
byRoot.set(root, (byRoot.get(root) ?? 0) + 1);
|
|
121
|
+
}
|
|
122
|
+
const sourceExtensions = [...byExtension.keys()].sort();
|
|
123
|
+
const languages = [...new Set(sourceExtensions.map((extension) => SOURCE_EXTENSIONS[extension]))].sort();
|
|
124
|
+
// A root earns governance by holding a real SHARE of the source, and the repository root earns it the same
|
|
125
|
+
// way as any other. Without that, one top-level packaging script (`setup.py`) qualifies the root, the root
|
|
126
|
+
// subsumes every sibling, and a library flat silently takes on its whole test and docs tree. The share is
|
|
127
|
+
// deliberately coarse — the emitted config is the user's to correct, and under-claiming costs one edit
|
|
128
|
+
// while over-claiming costs rounds spent specifying files nobody wanted specified.
|
|
129
|
+
const floor = Math.max(2, Math.ceil(fileCount * 0.05));
|
|
130
|
+
const qualified = [...byRoot.entries()].filter(([, count]) => count >= floor).map(([root]) => root).sort();
|
|
131
|
+
const governedRoots = qualified.includes('.') ? ['.'] : qualified;
|
|
132
|
+
// Report the count of what will actually BE governed, not of everything recognised: a file under a root that
|
|
133
|
+
// did not qualify is never gated, and counting it here would put the report permanently at odds with the
|
|
134
|
+
// gate's own denominator.
|
|
135
|
+
const governedCount = governedRoots.includes('.')
|
|
136
|
+
? fileCount
|
|
137
|
+
: governedRoots.reduce((total, root) => total + (byRoot.get(root) ?? 0), 0);
|
|
138
|
+
return { sourceExtensions, governedRoots, languages, fileCount: governedCount };
|
|
139
|
+
}
|
|
140
|
+
// The gate reading. `spex spec lint --json` exits non-zero when it found errors — that IS the reading, not a
|
|
141
|
+
// crash, so only an unparseable payload is a failure.
|
|
142
|
+
export function readGate(payload) {
|
|
143
|
+
const report = JSON.parse(payload);
|
|
144
|
+
const findings = report.findings ?? [];
|
|
145
|
+
const sourceFiles = report.sourceFiles ?? [];
|
|
146
|
+
const governed = sourceFiles.length;
|
|
147
|
+
const uncoveredFiles = findings.filter((f) => f.rule === 'coverage').map((f) => f.file ?? f.msg);
|
|
148
|
+
const errorFindings = findings.filter((f) => f.level === 'error').map((f) => `${f.rule}: ${f.msg}`);
|
|
149
|
+
return {
|
|
150
|
+
errors: errorFindings.length,
|
|
151
|
+
governed,
|
|
152
|
+
uncovered: uncoveredFiles.length,
|
|
153
|
+
coverage: governed ? Math.round(((governed - uncoveredFiles.length) / governed) * 100) : 0,
|
|
154
|
+
errorFindings,
|
|
155
|
+
uncoveredFiles,
|
|
156
|
+
sourceFiles,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export const gatePassed = (gate, coverageFloor) => gate.errors === 0 && gate.governed > 0 && gate.coverage >= coverageFloor;
|
|
160
|
+
// @@@ confirmProfile - narrow the PROPOSED governed set to the one lint actually keeps. The proposal is read
|
|
161
|
+
// off the file tree, but lint applies the product's own source policy on top (its testGlobs drop `tests/`,
|
|
162
|
+
// `test_*`, `*.test.*` wholesale), so a root can be proposed, written into the config, and then contribute
|
|
163
|
+
// nothing. Left uncorrected that root is a lie in two places at once: the emitted config lists a root the gate
|
|
164
|
+
// ignores, and the report prints a file count the gate's own denominator contradicts — measured on psf/requests
|
|
165
|
+
// as "37 source files across docs, src, tests" beside a gate reading of 21. The profile must be CONFIRMED
|
|
166
|
+
// against lint's accounting for the same reason convergence is: this file does not get to assert what is
|
|
167
|
+
// governed. Reimplementing the exclusion policy here instead would put a second copy of it in the tree, and the
|
|
168
|
+
// copies would drift.
|
|
169
|
+
export function confirmProfile(proposed, sourceFiles) {
|
|
170
|
+
const kept = new Set(sourceFiles.map((file) => (file.includes('/') ? file.split('/')[0] : '.')));
|
|
171
|
+
const governedRoots = proposed.governedRoots.filter((root) => root === '.' || kept.has(root));
|
|
172
|
+
return { ...proposed, governedRoots, fileCount: sourceFiles.length };
|
|
173
|
+
}
|
|
174
|
+
// The prose language is stated as its own paragraph rather than folded into the task, because it applies to
|
|
175
|
+
// every node and the one thing it must NOT touch is the ids.
|
|
176
|
+
const languageClause = (lang) => lang && lang.toLowerCase() !== 'en' ? [
|
|
177
|
+
``,
|
|
178
|
+
`WRITE THE SPEC PROSE IN ${languageName(lang)}. That means every node's \`title\`, \`desc\` and body — the`,
|
|
179
|
+
`whole tree reads in that language, not a translation layer over English.`,
|
|
180
|
+
`Node IDS STAY lowercase ascii-kebab (they are directory names and part of URLs), and so do the file paths`,
|
|
181
|
+
`in \`code:\`/\`related:\` and the \`[[id]]\` mentions that point at them.`,
|
|
182
|
+
] : [];
|
|
183
|
+
function surveyPrompt(profile, coverageFloor, lang) {
|
|
184
|
+
return [
|
|
185
|
+
`You are flattening this repository into a SpexCode .spec tree. This is the whole task; there is no other work.`,
|
|
186
|
+
``,
|
|
187
|
+
`FIRST, read the authoring format — it is the authority, not this message:`,
|
|
188
|
+
` spex guide spec`,
|
|
189
|
+
``,
|
|
190
|
+
`The repository is ${profile.languages.join(', ')} (${profile.fileCount} governed source files under ${profile.governedRoots.join(', ')}).`,
|
|
191
|
+
``,
|
|
192
|
+
`Write .spec nodes until every governed source file is claimed by exactly one node's \`code:\` list.`,
|
|
193
|
+
`Shape the tree the way the SYSTEM is shaped — one node per real responsibility, nesting where a`,
|
|
194
|
+
`responsibility genuinely contains sub-responsibilities. Do not mirror the directory layout for its own sake.`,
|
|
195
|
+
``,
|
|
196
|
+
`A node body states INTENT, INVARIANTS, and CONTRACTS: what this part guarantees, what it refuses, what`,
|
|
197
|
+
`would be a bug. It is not a paraphrase of the code — a reader who has the source already gets nothing`,
|
|
198
|
+
`from prose that restates it. Write what the source CANNOT tell them: why this shape, what it must never do.`,
|
|
199
|
+
...languageClause(lang),
|
|
200
|
+
``,
|
|
201
|
+
`You will be measured, and you will be told what failed:`,
|
|
202
|
+
` - \`spex spec lint\` must report ZERO errors.`,
|
|
203
|
+
` - Coverage must reach ${coverageFloor}% of governed source files.`,
|
|
204
|
+
` - \`spex doctor\` must not find nodes that dumped mechanics instead of stating intent.`,
|
|
205
|
+
``,
|
|
206
|
+
`Run those yourself as you go. Commit nothing; the driver commits.`,
|
|
207
|
+
].join('\n');
|
|
208
|
+
}
|
|
209
|
+
function repairPrompt(gate, doctor, coverageFloor, lang) {
|
|
210
|
+
const lines = [
|
|
211
|
+
`Continue flattening this repository. The gate was measured and it did NOT pass. Fix exactly what follows.`,
|
|
212
|
+
...languageClause(lang),
|
|
213
|
+
``,
|
|
214
|
+
`Coverage: ${gate.coverage}% of ${gate.governed} governed files (floor ${coverageFloor}%). Lint errors: ${gate.errors}.`,
|
|
215
|
+
];
|
|
216
|
+
if (gate.errorFindings.length) {
|
|
217
|
+
lines.push(``, `LINT ERRORS — these are structural and block everything:`);
|
|
218
|
+
for (const finding of gate.errorFindings.slice(0, 40))
|
|
219
|
+
lines.push(` - ${finding}`);
|
|
220
|
+
}
|
|
221
|
+
if (gate.uncoveredFiles.length) {
|
|
222
|
+
lines.push(``, `UNCLAIMED SOURCE FILES — each needs a node whose \`code:\` claims it:`);
|
|
223
|
+
for (const file of gate.uncoveredFiles.slice(0, 60))
|
|
224
|
+
lines.push(` - ${file}`);
|
|
225
|
+
if (gate.uncoveredFiles.length > 60)
|
|
226
|
+
lines.push(` … and ${gate.uncoveredFiles.length - 60} more; run \`spex spec lint\` for the rest.`);
|
|
227
|
+
}
|
|
228
|
+
if (doctor.trim()) {
|
|
229
|
+
lines.push(``, `SPEC HEALTH — nodes phrased as mechanics dumps rather than intent, and parents fanned too wide:`, ``, doctor.trim(), ``,
|
|
230
|
+
// `spex init` seeds .plugins with SpexCode's own workflow nodes, whose bodies ARE the prompt text that
|
|
231
|
+
// gets materialized for the agent. Doctor measures them like any other node, so its findings name them —
|
|
232
|
+
// and a round spent "fixing" them would trim live product behaviour and specify nothing about this
|
|
233
|
+
// repository. The boundary belongs in the instruction, not in a fragile filter over doctor's prose.
|
|
234
|
+
`IGNORE any finding under \`.spec/*/.plugins/\`. Those are SpexCode's own seeded workflow nodes, not`, `your work, and their bodies are prompt text that must not be trimmed. Fix only nodes you wrote.`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(``, `Commit nothing; the driver commits.`);
|
|
237
|
+
return lines.join('\n');
|
|
238
|
+
}
|
|
239
|
+
function configuredTurnHarness(name, root) {
|
|
240
|
+
const chosen = resolveLauncher(name, root);
|
|
241
|
+
const harness = harnessById(chosen.harness);
|
|
242
|
+
if (!harness.oneShotTurn) {
|
|
243
|
+
const capable = HARNESSES.filter((candidate) => candidate.oneShotTurn).map((candidate) => candidate.id).join(', ');
|
|
244
|
+
throw new Error(`spex flat: launcher '${name}' runs harness '${harness.id}', which has no non-interactive turn, so it ` +
|
|
245
|
+
`cannot run a conversion round. Harnesses that can: ${capable}. Pick a launcher on one of those with --launcher.`);
|
|
246
|
+
}
|
|
247
|
+
return { harness, cmd: chosen.cmd, name, harnessId: harness.id };
|
|
248
|
+
}
|
|
249
|
+
function builtinTurnHarness(name) {
|
|
250
|
+
const choice = FLAT_AGENT_CHOICES.find((candidate) => candidate.name === name);
|
|
251
|
+
if (!choice)
|
|
252
|
+
return null;
|
|
253
|
+
const harness = harnessById(choice.harness);
|
|
254
|
+
if (!harness.oneShotTurn)
|
|
255
|
+
return null;
|
|
256
|
+
return { harness, cmd: choice.cmd, name: choice.name, harnessId: choice.harness };
|
|
257
|
+
}
|
|
258
|
+
// The launcher IS the choice, exactly as it is for a session ([[launcher-select]]) when the caller has a
|
|
259
|
+
// project config. Flatcode also works as a global command from an unrelated directory, where no launcher
|
|
260
|
+
// registry exists yet: a regular agent name then names both its one-shot runner and the harness that init
|
|
261
|
+
// persists in the clone. There is no implicit fallback because a non-interactive run must never guess auth.
|
|
262
|
+
function resolveTurnHarness(launcher, root) {
|
|
263
|
+
if (launcher) {
|
|
264
|
+
const configured = launcherList(root);
|
|
265
|
+
if (configured.some((candidate) => candidate.name === launcher))
|
|
266
|
+
return configuredTurnHarness(launcher, root);
|
|
267
|
+
const builtin = builtinTurnHarness(launcher);
|
|
268
|
+
if (builtin)
|
|
269
|
+
return builtin;
|
|
270
|
+
if (configured.length)
|
|
271
|
+
return configuredTurnHarness(launcher, root);
|
|
272
|
+
throw new Error(`spex flat: unknown launcher '${launcher}'. Choose one of: ${FLAT_AGENT_CHOICES.map((choice) => choice.name).join(', ')}.`);
|
|
273
|
+
}
|
|
274
|
+
return configuredTurnHarness(defaultLauncher(root), root);
|
|
275
|
+
}
|
|
276
|
+
async function chooseTurnHarness(root, stdin = process.stdin, stdout = process.stdout) {
|
|
277
|
+
try {
|
|
278
|
+
return resolveTurnHarness(undefined, root);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error.message !== MISSING_DEFAULT_LAUNCHER_ERROR)
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
285
|
+
throw new Error(`spex flat: no configured launcher in ${root}. Pass --launcher <name> (${FLAT_AGENT_CHOICES.map((choice) => choice.name).join(', ')}).`);
|
|
286
|
+
}
|
|
287
|
+
stdout.write('Choose the agent that will convert this repository:\n');
|
|
288
|
+
for (const [index, choice] of FLAT_AGENT_CHOICES.entries())
|
|
289
|
+
stdout.write(` ${index + 1}. ${choice.label}\n`);
|
|
290
|
+
const prompt = createInterface({ input: stdin, output: stdout });
|
|
291
|
+
try {
|
|
292
|
+
const answer = (await prompt.question('Agent [1]: ')).trim() || '1';
|
|
293
|
+
const position = Number(answer);
|
|
294
|
+
const choice = Number.isInteger(position) ? FLAT_AGENT_CHOICES[position - 1] : undefined;
|
|
295
|
+
if (!choice)
|
|
296
|
+
throw new Error(`spex flat: expected a number from 1 to ${FLAT_AGENT_CHOICES.length}`);
|
|
297
|
+
return builtinTurnHarness(choice.name);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
prompt.close();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function git(args, cwd) {
|
|
304
|
+
return run('git', args, cwd);
|
|
305
|
+
}
|
|
306
|
+
async function gitOrThrow(args, cwd, what) {
|
|
307
|
+
const result = await git(args, cwd);
|
|
308
|
+
if (result.code !== 0)
|
|
309
|
+
throw new Error(`spex flat: ${what} failed — ${result.stderr.trim() || result.stdout.trim()}`);
|
|
310
|
+
return result.stdout.trim();
|
|
311
|
+
}
|
|
312
|
+
const isUrl = (target) => /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/.test(target);
|
|
313
|
+
export async function flatNew(options, log = console.log, choose = chooseTurnHarness) {
|
|
314
|
+
const rounds = options.rounds ?? DEFAULT_ROUNDS;
|
|
315
|
+
const coverageFloor = options.coverage ?? DEFAULT_COVERAGE;
|
|
316
|
+
if (!Number.isInteger(rounds) || rounds < 1)
|
|
317
|
+
throw new Error('spex flat: --rounds must be a positive integer');
|
|
318
|
+
if (!Number.isFinite(coverageFloor) || coverageFloor < 0 || coverageFloor > 100)
|
|
319
|
+
throw new Error('spex flat: --coverage must be between 0 and 100');
|
|
320
|
+
const name = basename(options.target.replace(/\.git$/, '').replace(/\/+$/, '')) || 'flat';
|
|
321
|
+
// --- acquire -------------------------------------------------------------------------------------------
|
|
322
|
+
// A local repository IS the intended home for its .spec tree. Flatcode only adds that governed intent; it
|
|
323
|
+
// never makes a throwaway copy of a repository the caller already has, and it refuses dirty state so its
|
|
324
|
+
// commits cannot accidentally absorb unrelated work. A remote URL has no such home, so it remains isolated
|
|
325
|
+
// in <out>/repo.
|
|
326
|
+
let source = options.target;
|
|
327
|
+
let local = null;
|
|
328
|
+
if (!isUrl(options.target)) {
|
|
329
|
+
local = resolve(options.target);
|
|
330
|
+
if (!existsSync(join(local, '.git')))
|
|
331
|
+
throw new Error(`spex flat: ${local} is not a git repository`);
|
|
332
|
+
const dirty = await gitOrThrow(['status', '--porcelain'], local, 'checking the working tree');
|
|
333
|
+
if (dirty)
|
|
334
|
+
throw new Error(`spex flat: ${local} has uncommitted changes — Flatcode commits a spec tree and will not mix it with work it did not write`);
|
|
335
|
+
source = local;
|
|
336
|
+
}
|
|
337
|
+
const out = resolve(options.out ?? (local ? join(dirname(local), `${basename(local)}.flat`) : `${name}.flat`));
|
|
338
|
+
const outputInsideSource = local && (() => {
|
|
339
|
+
const fromSource = relative(local, out);
|
|
340
|
+
return !fromSource || (!fromSource.startsWith(`..${sep}`) && fromSource !== '..' && !isAbsolute(fromSource));
|
|
341
|
+
})();
|
|
342
|
+
if (outputInsideSource)
|
|
343
|
+
throw new Error(`spex flat: ${out} is inside ${local}; Flatcode's output must stay beside the source repository`);
|
|
344
|
+
const repo = local ?? join(out, 'repo');
|
|
345
|
+
const configPath = join(repo, 'spexcode.json');
|
|
346
|
+
const initialized = Boolean(local && existsSync(join(repo, '.spec')) && existsSync(configPath));
|
|
347
|
+
if (local && existsSync(out)) {
|
|
348
|
+
const previous = join(out, 'flat.json');
|
|
349
|
+
const recorded = existsSync(previous) ? JSON.parse(readFileSync(previous, 'utf8')) : null;
|
|
350
|
+
if (recorded?.repo !== local)
|
|
351
|
+
throw new Error(`spex flat: ${out} already exists for another purpose — name a different --out`);
|
|
352
|
+
}
|
|
353
|
+
if (!local && existsSync(repo))
|
|
354
|
+
throw new Error(`spex flat: ${repo} already exists — name a different --out or remove it`);
|
|
355
|
+
// An existing SpexCode project names its own local launcher. A new local repository and a remote conversion
|
|
356
|
+
// inherit the launch boundary from the calling directory, whose configuration is the only one available yet.
|
|
357
|
+
const launcherRoot = local && existsSync(configPath) ? local : process.cwd();
|
|
358
|
+
const { harness, cmd, name: launcherName, harnessId } = options.launcher
|
|
359
|
+
? resolveTurnHarness(options.launcher, launcherRoot)
|
|
360
|
+
: await choose(launcherRoot);
|
|
361
|
+
if (local) {
|
|
362
|
+
log(`using local ${local}`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
mkdirSync(out, { recursive: true });
|
|
366
|
+
log(`cloning ${options.target}`);
|
|
367
|
+
const clone = await run('git', ['clone', '--quiet', options.target, repo], out);
|
|
368
|
+
if (clone.code !== 0)
|
|
369
|
+
throw new Error(`spex flat: clone failed — ${clone.stderr.trim()}`);
|
|
370
|
+
await gitOrThrow(['checkout', '--quiet', '-b', FLAT_BRANCH], repo, `creating the ${FLAT_BRANCH} branch`);
|
|
371
|
+
}
|
|
372
|
+
const revision = await gitOrThrow(['rev-parse', 'HEAD'], repo, 'identifying the cloned revision');
|
|
373
|
+
// --- profile -------------------------------------------------------------------------------------------
|
|
374
|
+
const tracked = (await gitOrThrow(['ls-files'], repo, 'listing tracked files')).split('\n').filter(Boolean);
|
|
375
|
+
const proposed = profileFiles(tracked);
|
|
376
|
+
if (!proposed.fileCount) {
|
|
377
|
+
throw new Error(`spex flat: no recognised source files in ${source}. Flatcode governs a fixed allowlist of source ` +
|
|
378
|
+
`extensions; a repository outside it would gate on an empty governed set, which passes vacuously.`);
|
|
379
|
+
}
|
|
380
|
+
// --- seed or resume ------------------------------------------------------------------------------------
|
|
381
|
+
// An initialized local project already has a .spec tree, policy, and launcher choice. It is not a fresh
|
|
382
|
+
// adopter merely because Flatcode has not run there: preserve those choices exactly and let the gate tell
|
|
383
|
+
// the agent what remains. Every other target receives the ordinary adoption seed before its first turn.
|
|
384
|
+
const writeConfig = (chosen) => {
|
|
385
|
+
const existing = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : {};
|
|
386
|
+
writeFileSync(configPath, `${JSON.stringify({
|
|
387
|
+
...existing,
|
|
388
|
+
// Name the graph after the repository it reads, not after Flatcode's checkout directory — otherwise every
|
|
389
|
+
// flat in the world renders under the title "repo".
|
|
390
|
+
dashboard: { title: name, ...(existing.dashboard ?? {}) },
|
|
391
|
+
lint: { ...(existing.lint ?? {}), governedRoots: chosen.governedRoots, sourceExtensions: chosen.sourceExtensions },
|
|
392
|
+
}, null, 2)}\n`);
|
|
393
|
+
};
|
|
394
|
+
let gate;
|
|
395
|
+
let profile;
|
|
396
|
+
if (initialized) {
|
|
397
|
+
gate = await gateOf(repo);
|
|
398
|
+
profile = profileFiles(gate.sourceFiles);
|
|
399
|
+
log(`resuming existing .spec · launcher ${launcherName} (${harness.id})`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
const init = await spex(['init', '--harness', harnessId], repo);
|
|
403
|
+
if (init.code !== 0)
|
|
404
|
+
throw new Error(`spex flat: seeding .spec failed — ${init.stderr.trim() || init.stdout.trim()}`);
|
|
405
|
+
log(`seeded .spec · launcher ${launcherName} (${harness.id})`);
|
|
406
|
+
// Init must run before the profile write. A fresh clone has no config, and init is the one operation that
|
|
407
|
+
// seeds the selected harness plus its matching named launcher. Writing a partial config first made init
|
|
408
|
+
// preserve that file, leaving the clone with a harness target but no launcher profile for the agent it used.
|
|
409
|
+
writeConfig(proposed);
|
|
410
|
+
await commit(repo, 'flatcode: profile and seed .spec', ['.spec', 'spexcode.json']);
|
|
411
|
+
gate = await gateOf(repo);
|
|
412
|
+
profile = confirmProfile(proposed, gate.sourceFiles);
|
|
413
|
+
const dropped = proposed.governedRoots.filter((root) => !profile.governedRoots.includes(root));
|
|
414
|
+
if (dropped.length) {
|
|
415
|
+
writeConfig(profile);
|
|
416
|
+
await commit(repo, `flatcode: narrow governed roots to ${profile.governedRoots.join(', ')}`, ['spexcode.json']);
|
|
417
|
+
gate = await gateOf(repo);
|
|
418
|
+
log(`dropped ${dropped.join(', ')} — lint's source policy governs nothing there`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!gate.governed) {
|
|
422
|
+
throw new Error(`spex flat: lint governs no source file in ${source}, so the gate would pass vacuously. The proposed ` +
|
|
423
|
+
`roots were ${proposed.governedRoots.join(', ')}; the product's source policy kept none of them.`);
|
|
424
|
+
}
|
|
425
|
+
log(`governing ${profile.fileCount} source files · ${profile.languages.join(', ')} · roots ${profile.governedRoots.join(', ')}`);
|
|
426
|
+
// --- converge ------------------------------------------------------------------------------------------
|
|
427
|
+
let round = 0;
|
|
428
|
+
while (round < rounds && !gatePassed(gate, coverageFloor)) {
|
|
429
|
+
round += 1;
|
|
430
|
+
const doctor = round === 1 ? '' : (await spex(['doctor'], repo)).stdout;
|
|
431
|
+
const prompt = round === 1 ? surveyPrompt(profile, coverageFloor, options.lang) : repairPrompt(gate, doctor, coverageFloor, options.lang);
|
|
432
|
+
log(`round ${round}/${rounds} — coverage ${gate.coverage}% · ${gate.errors} lint error(s)`);
|
|
433
|
+
const before = await gitOrThrow(['rev-parse', 'HEAD'], repo, 'recording the round start');
|
|
434
|
+
const beforeUntracked = new Set((await gitOrThrow(['ls-files', '--others', '--exclude-standard'], repo, 'recording untracked files')).split('\n').filter(Boolean));
|
|
435
|
+
const code = await runTurn(harness.oneShotTurn(prompt, cmd), repo);
|
|
436
|
+
if (code !== 0)
|
|
437
|
+
log(`round ${round}: the agent turn exited ${code}; measuring anyway`);
|
|
438
|
+
const after = await gitOrThrow(['rev-parse', 'HEAD'], repo, 'checking the agent round');
|
|
439
|
+
if (after !== before)
|
|
440
|
+
await gitOrThrow(['reset', '--soft', before], repo, 'taking ownership of the agent commit');
|
|
441
|
+
await assertOnlyFlatChanges(repo, before, beforeUntracked);
|
|
442
|
+
await commit(repo, `flatcode: round ${round}`, ['.spec']);
|
|
443
|
+
gate = await gateOf(repo);
|
|
444
|
+
}
|
|
445
|
+
const passed = gatePassed(gate, coverageFloor);
|
|
446
|
+
mkdirSync(out, { recursive: true });
|
|
447
|
+
const result = { out, repo, source, revision, rounds: round, gate, passed, profile };
|
|
448
|
+
writeFileSync(join(out, 'flat.json'), `${JSON.stringify({
|
|
449
|
+
schema: 'spexcode.flat/v1', repo,
|
|
450
|
+
source, revision, branch: FLAT_BRANCH, launcher: launcherName, harness: harness.id,
|
|
451
|
+
rounds: round, roundBudget: rounds, coverageFloor, passed, lang: options.lang ?? 'en',
|
|
452
|
+
gate: { errors: gate.errors, governed: gate.governed, uncovered: gate.uncovered, coverage: gate.coverage },
|
|
453
|
+
profile,
|
|
454
|
+
}, null, 2)}\n`);
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
// The graph-only shell belongs to the dashboard package; its static bytes remain independent from a flat's payload.
|
|
458
|
+
export function publicShellDir() {
|
|
459
|
+
return ensureDashboardArtifact('dist-public');
|
|
460
|
+
}
|
|
461
|
+
// The one spec-root directory under .spec. `spex init` names it for the project, so it is `project` in a
|
|
462
|
+
// fresh flat and `spexcode` in this repository — deriving it beats hardcoding either.
|
|
463
|
+
function specRoot(repo) {
|
|
464
|
+
const entries = readdirSync(join(repo, '.spec'), { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
465
|
+
if (entries.length !== 1)
|
|
466
|
+
throw new Error(`spex flat site: expected exactly one spec root under .spec, found ${entries.length}`);
|
|
467
|
+
return entries[0].name;
|
|
468
|
+
}
|
|
469
|
+
export async function flatSite(flatDir, log = console.log) {
|
|
470
|
+
const out = resolve(flatDir);
|
|
471
|
+
const recordPath = join(out, 'flat.json');
|
|
472
|
+
if (!existsSync(recordPath))
|
|
473
|
+
throw new Error(`spex flat site: ${out} is not a flat — no flat.json. Run \`spex flat new\` first.`);
|
|
474
|
+
const record = JSON.parse(readFileSync(recordPath, 'utf8'));
|
|
475
|
+
const repo = record.repo && existsSync(record.repo) ? record.repo : join(out, 'repo');
|
|
476
|
+
const site = join(out, 'site');
|
|
477
|
+
const shell = publicShellDir();
|
|
478
|
+
mkdirSync(site, { recursive: true });
|
|
479
|
+
cpSync(join(shell, 'index.html'), join(site, 'index.html'));
|
|
480
|
+
cpSync(join(shell, 'assets'), join(site, 'assets'), { recursive: true });
|
|
481
|
+
const graphPath = join(site, 'public-graph.json');
|
|
482
|
+
const graph = await spex(['graph', '--public', '--out', graphPath, '--content-dir', join(site, 'specs')], repo);
|
|
483
|
+
if (graph.code !== 0)
|
|
484
|
+
throw new Error(`spex flat site: building the public graph failed — ${graph.stderr.trim() || graph.stdout.trim()}`);
|
|
485
|
+
const root = specRoot(repo);
|
|
486
|
+
// @@@ the publication is the repository's spec, not SpexCode's - `spex init` seeds .plugins with SpexCode's
|
|
487
|
+
// own workflow nodes so the converting agent receives its contract. They are machinery, not a reading of the
|
|
488
|
+
// target: measured on the first three published flats they were 38-48% of every graph, and their bodies ARE
|
|
489
|
+
// the command prompt texts, so a visitor who came to read `requests` met SpexCode's `extract` prompt
|
|
490
|
+
// presented as that repository's specification. The conversion needs them; the publication must not carry
|
|
491
|
+
// them. Filtering here rather than at seed time keeps the agent's contract intact.
|
|
492
|
+
const payload = JSON.parse(readFileSync(graphPath, 'utf8'));
|
|
493
|
+
// The graph's `path` is repo-relative and includes `.spec/`, so the prefix has to as well.
|
|
494
|
+
const seeded = `.spec/${root}/.plugins/`;
|
|
495
|
+
const published = payload.nodes.filter((node) => !node.path.startsWith(seeded));
|
|
496
|
+
if (published.length === payload.nodes.length) {
|
|
497
|
+
throw new Error(`spex flat site: nothing matched the seeded prefix ${seeded} — the publication would carry SpexCode's own workflow nodes as if they were this repository's spec`);
|
|
498
|
+
}
|
|
499
|
+
const dropped = payload.nodes.length - published.length;
|
|
500
|
+
if (dropped) {
|
|
501
|
+
for (const node of payload.nodes) {
|
|
502
|
+
if (published.includes(node))
|
|
503
|
+
continue;
|
|
504
|
+
rmSync(join(site, 'specs', `${node.id}.json`), { force: true });
|
|
505
|
+
}
|
|
506
|
+
writeFileSync(graphPath, `${JSON.stringify({ ...payload, nodes: published }, null, 2)}\n`);
|
|
507
|
+
log(` dropped ${dropped} seeded SpexCode workflow node(s) from the publication`);
|
|
508
|
+
}
|
|
509
|
+
const archiveName = `${root}.spec.zip`;
|
|
510
|
+
// The archive is the same publication, so it excludes the same subtree — a downloader who unzips it must
|
|
511
|
+
// get what the graph showed, not the machinery the graph deliberately left out.
|
|
512
|
+
const topLevel = readdirSync(join(repo, '.spec', root), { withFileTypes: true })
|
|
513
|
+
.map((entry) => entry.name)
|
|
514
|
+
.filter((name) => name !== '.plugins')
|
|
515
|
+
.sort();
|
|
516
|
+
const archive = await run('git', [
|
|
517
|
+
'archive', '--format=zip', '--prefix=.spec/', `--output=${join(site, archiveName)}`,
|
|
518
|
+
`${payload.revision}:.spec/${root}`, ...topLevel,
|
|
519
|
+
], repo);
|
|
520
|
+
if (archive.code !== 0)
|
|
521
|
+
throw new Error(`spex flat site: archiving the spec tree failed — ${archive.stderr.trim()}`);
|
|
522
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
523
|
+
const asset = (path) => { const bytes = readFileSync(join(site, path)); return { path, bytes: bytes.byteLength, sha256: sha256(bytes) }; };
|
|
524
|
+
const graphAsset = asset('public-graph.json');
|
|
525
|
+
const archiveAsset = { ...asset(archiveName), name: archiveName };
|
|
526
|
+
const documents = readdirSync(join(site, 'specs')).filter((name) => name.endsWith('.json')).sort();
|
|
527
|
+
// The About panel's facts are the flat's own reading. A visitor should be able to see, without leaving the
|
|
528
|
+
// page, that this tree was produced by a measured conversion and how complete that conversion actually was —
|
|
529
|
+
// publishing the graph while hiding its coverage would present a partial flat as a finished one.
|
|
530
|
+
const metadata = {
|
|
531
|
+
schema: 'spexcode.public-spec-site/v1',
|
|
532
|
+
// A repository link is offered only when the source is one a browser can follow. A local path rendered as
|
|
533
|
+
// a link would promise a destination that does not exist for anyone but the person who ran the flat.
|
|
534
|
+
publication: { id: root, ...(isUrl(record.source) ? { repository: { url: record.source } } : {}) },
|
|
535
|
+
about: {
|
|
536
|
+
title: `About this flat`,
|
|
537
|
+
summary: 'A static, read-only view of a specification graph produced by Flatcode from the repository named below. It exposes committed spec intent and relationships only; sessions, issues, evaluations, settings, and write routes are absent.',
|
|
538
|
+
facts: [
|
|
539
|
+
{ label: 'Source', value: record.source },
|
|
540
|
+
// The panel renders the release revision itself; a second one here would read as two different commits.
|
|
541
|
+
{ label: 'Source revision', value: record.revision.slice(0, 12) },
|
|
542
|
+
{ label: 'Coverage', value: `${record.gate?.coverage ?? 0}% of ${record.gate?.governed ?? 0} governed source files` },
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
release: { revision: payload.revision, graph: graphAsset, archive: archiveAsset },
|
|
546
|
+
};
|
|
547
|
+
const metadataBytes = Buffer.from(`${JSON.stringify(metadata, null, 2)}\n`);
|
|
548
|
+
writeFileSync(join(site, 'public-graph-meta.json'), metadataBytes);
|
|
549
|
+
writeFileSync(join(site, 'public-spec-release.json'), `${JSON.stringify({
|
|
550
|
+
schema: 'spexcode.public-spec-release/v1',
|
|
551
|
+
revision: payload.revision,
|
|
552
|
+
publication: metadata.publication,
|
|
553
|
+
graph: graphAsset,
|
|
554
|
+
metadata: { path: 'public-graph-meta.json', bytes: metadataBytes.byteLength, sha256: sha256(metadataBytes) },
|
|
555
|
+
archive: archiveAsset,
|
|
556
|
+
documents: documents.map((name) => asset(`specs/${name}`)),
|
|
557
|
+
}, null, 2)}\n`);
|
|
558
|
+
log(`site: ${published.length} nodes at ${payload.revision.slice(0, 12)} → ${site}`);
|
|
559
|
+
return { site, nodes: published.length };
|
|
560
|
+
}
|
|
561
|
+
// @@@ gallerySlug - the path a flat is served at. Derived from the SOURCE the flat read, never from the
|
|
562
|
+
// directory Flatcode happened to write into: two people flattening the same repository must land on the same
|
|
563
|
+
// slug, and a local `--out` name is an accident of one machine.
|
|
564
|
+
export function gallerySlug(source) {
|
|
565
|
+
const url = source.replace(/\.git$/, '').replace(/\/+$/, '');
|
|
566
|
+
const forge = /^(?:https?:\/\/|git@|ssh:\/\/|git:\/\/)[^/:]+[:/](.+)$/.exec(url);
|
|
567
|
+
const raw = forge ? forge[1] : basename(url);
|
|
568
|
+
// Sanitize per SEGMENT, not over the whole string. The slug becomes a directory under the gallery root and
|
|
569
|
+
// a path on a public host, and the source it comes from is attacker-controllable; treating it as one string
|
|
570
|
+
// turns `../../etc/passwd` into `/-/etc/passwd`, which is absolute. Splitting first means a traversal
|
|
571
|
+
// segment cannot survive as one: `..` sanitizes to empty and is dropped, as is any empty or dot segment.
|
|
572
|
+
const segments = raw.split('/')
|
|
573
|
+
.map((segment) => segment.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''))
|
|
574
|
+
.filter((segment) => segment && segment !== '.' && segment !== '..');
|
|
575
|
+
return segments.join('/') || 'flat';
|
|
576
|
+
}
|
|
577
|
+
const escapeHtml = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
578
|
+
// The colours developers already read a language by. Anything unlisted gets the neutral dot rather than a
|
|
579
|
+
// guessed hue — a wrong-but-confident colour is worse than none.
|
|
580
|
+
const LANGUAGE_DOT = {
|
|
581
|
+
TypeScript: '#3178c6', JavaScript: '#f1e05a', Python: '#3572a5', Go: '#00add8', Rust: '#dea584',
|
|
582
|
+
Ruby: '#701516', Java: '#b07219', Kotlin: '#a97bff', Swift: '#f05138', C: '#555555', 'C++': '#f34b7d',
|
|
583
|
+
'C#': '#178600', PHP: '#4f5d95', Scala: '#c22d40', Elixir: '#6e4a7e', Erlang: '#b83998',
|
|
584
|
+
Haskell: '#5e5086', OCaml: '#ef7a08', Clojure: '#db5855', Lua: '#000080', Dart: '#00b4ab',
|
|
585
|
+
Vue: '#41b883', Svelte: '#ff3e00', Shell: '#89e051', Zig: '#ec915c',
|
|
586
|
+
};
|
|
587
|
+
// The index is hand-written rather than another dashboard build: it is a LIST, not a graph, and giving it the
|
|
588
|
+
// graph bundle would ship a megabyte of react-flow to render eight links. Self-contained and theme-aware for
|
|
589
|
+
// the same reason every published artifact here is — it must survive on a static host with no build step.
|
|
590
|
+
export function galleryIndexHtml(entries) {
|
|
591
|
+
const cards = entries.map((entry) => {
|
|
592
|
+
const [owner, name] = entry.slug.includes('/') ? [entry.slug.split('/')[0], entry.slug.split('/').slice(1).join('/')] : ['', entry.slug];
|
|
593
|
+
const langs = entry.languages.map((language) => `<span class="lang"><i style="background:${LANGUAGE_DOT[language] ?? '#6b7280'}"></i>${escapeHtml(language)}</span>`).join('');
|
|
594
|
+
return ` <a class="card" href="./${escapeHtml(entry.slug)}/">
|
|
595
|
+
<div class="card-top">
|
|
596
|
+
<h3>${owner ? `<span class="owner">${escapeHtml(owner)}/</span>` : ''}${escapeHtml(name)}</h3>
|
|
597
|
+
<svg class="go" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="langs">${langs}</div>
|
|
600
|
+
<div class="stats">
|
|
601
|
+
<span><b>${entry.nodes}</b> 个节点</span>
|
|
602
|
+
<span><b>${entry.governed}</b> 个文件</span>
|
|
603
|
+
<span class="${entry.passed ? 'ok' : 'partial'}">${entry.coverage}% 覆盖${entry.passed ? '' : '(部分)'}</span>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="rev"><code>${escapeHtml(entry.revision.slice(0, 12))}</code></div>
|
|
606
|
+
</a>`;
|
|
607
|
+
}).join('\n');
|
|
608
|
+
return `<!doctype html>
|
|
609
|
+
<html lang="zh-CN">
|
|
610
|
+
<head>
|
|
611
|
+
<meta charset="utf-8">
|
|
612
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
613
|
+
<title>Flatcode 软件二向箔 | 代码库说明</title>
|
|
614
|
+
<meta name="description" content="Flatcode 为代码库生成 .spec 说明,并检查说明的结构和源码覆盖情况。">
|
|
615
|
+
<!-- Inlined, because a static host with no favicon answers a 404 on every single visit. The mark is the
|
|
616
|
+
name: a solid body above, flattened to a line below. -->
|
|
617
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%2310b981'/%3E%3Crect x='8' y='7' width='16' height='9' rx='2' fill='%23041b12' opacity='.75'/%3E%3Crect x='6' y='21' width='20' height='3.5' rx='1.75' fill='%23041b12'/%3E%3C/svg%3E">
|
|
618
|
+
<style>
|
|
619
|
+
/* Committed to dark on purpose: the graph pages a visitor clicks into are dark, and a light shell handing
|
|
620
|
+
off to a dark app is the seam that makes a site feel assembled rather than made. Every colour is painted
|
|
621
|
+
explicitly so nothing inherits the host's theme. */
|
|
622
|
+
:root {
|
|
623
|
+
--bg: #08090b; --panel: #0e1013; --line: #1c1f24; --line-hi: #2b3038;
|
|
624
|
+
--fg: #f2f3f5; --muted: #9096a1; --dim: #6b7280;
|
|
625
|
+
--accent: #10b981; --accent-soft: #34d399;
|
|
626
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
627
|
+
--sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue",
|
|
628
|
+
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
629
|
+
}
|
|
630
|
+
* { box-sizing: border-box; }
|
|
631
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
632
|
+
body {
|
|
633
|
+
margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans);
|
|
634
|
+
font-size: 16px; line-height: 1.6; -webkit-font-smoothing: antialiased;
|
|
635
|
+
}
|
|
636
|
+
.wrap { max-width: 68rem; margin: 0 auto; padding: 0 1.5rem; }
|
|
637
|
+
|
|
638
|
+
nav { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem 0; }
|
|
639
|
+
.brand { display: flex; align-items: center; gap: .6rem; font-weight: 600; }
|
|
640
|
+
.brand svg { display: block; }
|
|
641
|
+
.nav-links { display: flex; align-items: center; gap: .5rem; }
|
|
642
|
+
nav a.ghost {
|
|
643
|
+
display: inline-flex; align-items: center; gap: .4rem;
|
|
644
|
+
color: var(--muted); text-decoration: none; font-size: .875rem; padding: .45rem .85rem;
|
|
645
|
+
border: 1px solid var(--line); border-radius: 8px; transition: border-color .15s, color .15s;
|
|
646
|
+
}
|
|
647
|
+
nav a.ghost.icon { padding: .45rem .6rem; }
|
|
648
|
+
nav a.ghost:hover { color: var(--fg); border-color: var(--line-hi); }
|
|
649
|
+
|
|
650
|
+
header.hero {
|
|
651
|
+
position: relative; isolation: isolate; overflow: hidden; min-height: min(33rem, calc(100vh - 9rem));
|
|
652
|
+
margin: 0 calc(50% - 50vw) 4rem; padding: 6rem max(1.5rem, calc((100vw - 68rem) / 2)); display: flex;
|
|
653
|
+
align-items: center; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
|
|
654
|
+
}
|
|
655
|
+
.hero-image { position: absolute; z-index: -2; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
|
|
656
|
+
.hero-shade { position: absolute; z-index: -1; inset: 0; background: rgba(4, 6, 7, .56); }
|
|
657
|
+
.hero-content { max-width: 38rem; }
|
|
658
|
+
.eyebrow {
|
|
659
|
+
font-size: .8125rem; color: var(--accent-soft); margin-bottom: 1.25rem;
|
|
660
|
+
}
|
|
661
|
+
h1 {
|
|
662
|
+
font-size: 4.25rem; line-height: 1.02;
|
|
663
|
+
font-weight: 620; margin: 0 0 1.25rem;
|
|
664
|
+
}
|
|
665
|
+
h1 em { font-style: normal; color: var(--accent-soft); }
|
|
666
|
+
.lede { font-size: 1.125rem; color: #c0c5cc; margin: 0; max-width: 35rem; }
|
|
667
|
+
|
|
668
|
+
section.onboarding { padding-bottom: 4.5rem; }
|
|
669
|
+
.section-label { color: var(--accent-soft); font-family: var(--mono); font-size: .75rem; margin: 0 0 .6rem; }
|
|
670
|
+
.section-intro { max-width: 39rem; color: var(--muted); margin: 0 0 2.25rem; }
|
|
671
|
+
.setup-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
672
|
+
.setup-step { min-height: 15rem; padding: 1.5rem 1.25rem 1.75rem 0; border-right: 1px solid var(--line); }
|
|
673
|
+
.setup-step + .setup-step { padding-left: 1.25rem; }
|
|
674
|
+
.setup-step:last-child { border-right: 0; }
|
|
675
|
+
.setup-step h3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 600; }
|
|
676
|
+
.setup-step p { color: var(--muted); font-size: .875rem; margin: .8rem 0 0; }
|
|
677
|
+
.step-number { color: var(--dim); font-family: var(--mono); font-size: .75rem; display: block; margin-bottom: .6rem; }
|
|
678
|
+
.agent-choice { display: block; color: var(--muted); font-size: .8125rem; margin-bottom: .45rem; }
|
|
679
|
+
.agent-select { position: relative; }
|
|
680
|
+
.agent-select::after {
|
|
681
|
+
content: ""; position: absolute; right: .85rem; top: 50%; width: .48rem; height: .48rem;
|
|
682
|
+
border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
|
|
683
|
+
transform: translateY(-65%) rotate(45deg); pointer-events: none;
|
|
684
|
+
}
|
|
685
|
+
.agent-select select {
|
|
686
|
+
appearance: none; width: 100%; min-height: 2.75rem; color: var(--fg); background: var(--panel);
|
|
687
|
+
border: 1px solid var(--line-hi); border-radius: 8px; padding: .55rem 2.5rem .55rem .75rem;
|
|
688
|
+
font: inherit; cursor: pointer; transition: border-color .15s, box-shadow .15s, background .15s;
|
|
689
|
+
}
|
|
690
|
+
.agent-select select:hover { background: #12151a; border-color: #3b434e; }
|
|
691
|
+
.agent-select select:focus-visible {
|
|
692
|
+
outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(16, 185, 129, .18);
|
|
693
|
+
}
|
|
694
|
+
.agent-select select option { color: var(--fg); background: var(--panel);
|
|
695
|
+
}
|
|
696
|
+
.command-row { display: flex; align-items: center; gap: .5rem; margin-top: .85rem; }
|
|
697
|
+
.cmd {
|
|
698
|
+
display: flex; align-items: center; gap: .5rem; background: var(--panel); border: 1px solid var(--line);
|
|
699
|
+
border-radius: 8px; padding: .75rem .8rem; font-family: var(--mono); font-size: .8125rem;
|
|
700
|
+
min-width: 0; flex: 1; text-align: left;
|
|
701
|
+
}
|
|
702
|
+
.cmd .prompt { color: var(--accent); user-select: none; }
|
|
703
|
+
.cmd code { color: var(--fg); white-space: nowrap; overflow-x: auto; flex: 1; }
|
|
704
|
+
.copy-button {
|
|
705
|
+
flex: none; width: 2.5rem; height: 2.5rem; display: inline-grid; place-items: center; background: var(--panel);
|
|
706
|
+
border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer;
|
|
707
|
+
transition: color .15s, border-color .15s;
|
|
708
|
+
}
|
|
709
|
+
.copy-button:hover { color: var(--fg); border-color: var(--line-hi); }
|
|
710
|
+
.copy-button svg { width: 1rem; height: 1rem; }
|
|
711
|
+
|
|
712
|
+
section.gallery { padding-bottom: 6rem; }
|
|
713
|
+
.sec-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 1.5rem; }
|
|
714
|
+
.sec-head h2 { font-size: 1.0625rem; font-weight: 600; margin: 0; }
|
|
715
|
+
.sec-head span { color: var(--dim); font-size: .875rem; }
|
|
716
|
+
|
|
717
|
+
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); }
|
|
718
|
+
.card {
|
|
719
|
+
display: block; padding: 1.25rem; background: var(--panel); border: 1px solid var(--line);
|
|
720
|
+
border-radius: 8px; color: inherit; text-decoration: none;
|
|
721
|
+
transition: border-color .18s, transform .18s, background .18s;
|
|
722
|
+
}
|
|
723
|
+
.card:hover { border-color: var(--line-hi); background: #12151a; transform: translateY(-2px); }
|
|
724
|
+
.card:hover .go { color: var(--accent-soft); transform: translateX(2px); }
|
|
725
|
+
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
726
|
+
.card h3 { margin: 0; font-size: 1rem; font-weight: 600; overflow-wrap: anywhere; }
|
|
727
|
+
.card .owner { color: var(--dim); font-weight: 400; }
|
|
728
|
+
.go { color: var(--dim); flex: none; transition: color .18s, transform .18s; }
|
|
729
|
+
.langs { display: flex; flex-wrap: wrap; gap: .5rem; margin: .85rem 0 1rem; }
|
|
730
|
+
.lang { display: inline-flex; align-items: center; gap: .35rem; font-size: .75rem; color: var(--muted); }
|
|
731
|
+
.lang i { width: .5rem; height: .5rem; border-radius: 50%; display: inline-block; }
|
|
732
|
+
.stats { display: flex; flex-wrap: wrap; gap: .9rem; font-size: .8125rem; color: var(--muted); }
|
|
733
|
+
.stats b { color: var(--fg); font-weight: 600; }
|
|
734
|
+
.stats .ok { color: var(--accent-soft); }
|
|
735
|
+
.stats .partial { color: #fbbf24; }
|
|
736
|
+
.rev { margin-top: .8rem; font-family: var(--mono); font-size: .6875rem; color: #4b5563; }
|
|
737
|
+
|
|
738
|
+
footer { border-top: 1px solid var(--line); padding: 2rem 0 3.5rem; color: var(--dim); font-size: .875rem; }
|
|
739
|
+
footer a { color: var(--muted); text-decoration: none; }
|
|
740
|
+
footer a:hover { color: var(--fg); }
|
|
741
|
+
@media (max-width: 640px) {
|
|
742
|
+
nav { padding: 1rem 0; }
|
|
743
|
+
nav a.ghost { padding: .4rem .55rem; }
|
|
744
|
+
header.hero { min-height: 29rem; margin-bottom: 3rem; padding-top: 4rem; padding-bottom: 3rem; }
|
|
745
|
+
.hero-image { object-position: 66% center; }
|
|
746
|
+
.hero-shade { background: rgba(4, 6, 7, .66); }
|
|
747
|
+
h1 { font-size: 2.625rem; }
|
|
748
|
+
.lede { font-size: 1rem; }
|
|
749
|
+
.setup-grid { grid-template-columns: 1fr; }
|
|
750
|
+
.setup-step, .setup-step + .setup-step { min-height: 0; padding: 1.4rem 0; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
751
|
+
.setup-step:last-child { border-bottom: 0; }
|
|
752
|
+
.cmd { font-size: .75rem; }
|
|
753
|
+
}
|
|
754
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
755
|
+
</style>
|
|
756
|
+
</head>
|
|
757
|
+
<body>
|
|
758
|
+
<div class="wrap">
|
|
759
|
+
<nav>
|
|
760
|
+
<div class="brand">
|
|
761
|
+
<svg width="22" height="22" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7" fill="#10b981"/><rect x="8" y="7" width="16" height="9" rx="2" fill="#041b12" opacity=".75"/><rect x="6" y="21" width="20" height="3.5" rx="1.75" fill="#041b12"/></svg>
|
|
762
|
+
Flatcode
|
|
763
|
+
</div>
|
|
764
|
+
<div class="nav-links">
|
|
765
|
+
<a class="ghost" href="https://spexcode.net/zh/flatcode/">文档</a>
|
|
766
|
+
<a class="ghost icon" href="https://github.com/shuxueshuxue/spexcode" target="_blank" rel="noopener noreferrer" aria-label="GitHub">
|
|
767
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
|
|
768
|
+
</a>
|
|
769
|
+
</div>
|
|
770
|
+
</nav>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<header class="hero">
|
|
774
|
+
<img class="hero-image" src="./flatcode-banner.webp" alt="代码库整理为规格图谱">
|
|
775
|
+
<div class="hero-shade"></div>
|
|
776
|
+
<div class="hero-content">
|
|
777
|
+
<div class="eyebrow">软件二向箔,基于 SpexCode</div>
|
|
778
|
+
<h1>代码库的<em>职责说明</em>。</h1>
|
|
779
|
+
<p class="lede">
|
|
780
|
+
Flatcode 分析一个仓库,为其中的功能和模块生成 .spec 说明。
|
|
781
|
+
它会检查说明的结构和对源码的覆盖情况,未通过检查的结果会标为部分完成。
|
|
782
|
+
</p>
|
|
783
|
+
</div>
|
|
784
|
+
</header>
|
|
785
|
+
|
|
786
|
+
<div class="wrap">
|
|
787
|
+
<section class="onboarding" aria-labelledby="start-title">
|
|
788
|
+
<p class="section-label">开始转换</p>
|
|
789
|
+
<h2 id="start-title">安装后选择本机 agent</h2>
|
|
790
|
+
<p class="section-intro">只需安装一次。仓库 URL 会被克隆并初始化;本地仓库则直接补全 .spec,已有设置会保留。</p>
|
|
791
|
+
<div class="setup-grid">
|
|
792
|
+
<div class="setup-step">
|
|
793
|
+
<span class="step-number">01</span>
|
|
794
|
+
<h3>安装 SpexCode</h3>
|
|
795
|
+
<div class="cmd"><span class="prompt">$</span><code>npm i -g spexcode</code></div>
|
|
796
|
+
<p>需要 Node 22 或更高版本和 git。</p>
|
|
797
|
+
</div>
|
|
798
|
+
<div class="setup-step">
|
|
799
|
+
<span class="step-number">02</span>
|
|
800
|
+
<h3>选择 agent</h3>
|
|
801
|
+
<label class="agent-choice" for="agent">用于读取源码和生成说明</label>
|
|
802
|
+
<div class="agent-select">
|
|
803
|
+
<select id="agent" name="agent">
|
|
804
|
+
<option value="claude">Claude Code</option>
|
|
805
|
+
<option value="codex">Codex</option>
|
|
806
|
+
<option value="opencode">OpenCode</option>
|
|
807
|
+
<option value="pi">Pi</option>
|
|
808
|
+
</select>
|
|
809
|
+
</div>
|
|
810
|
+
<p>也可以在终端直接运行命令后再选择。</p>
|
|
811
|
+
</div>
|
|
812
|
+
<div class="setup-step">
|
|
813
|
+
<span class="step-number">03</span>
|
|
814
|
+
<h3>转换仓库</h3>
|
|
815
|
+
<div class="command-row">
|
|
816
|
+
<div class="cmd"><span class="prompt">$</span><code id="cmd">spex flat new https://github.com/owner/repo --launcher claude</code></div>
|
|
817
|
+
<button type="button" class="copy-button" id="copy" aria-label="复制命令" title="复制命令">
|
|
818
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
819
|
+
</button>
|
|
820
|
+
</div>
|
|
821
|
+
<p>URL 的结果放在独立目录;本地仓库只会提交 .spec,不会推送。</p>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
</section>
|
|
825
|
+
|
|
826
|
+
<section class="gallery">
|
|
827
|
+
<div class="sec-head">
|
|
828
|
+
<h2>已生成说明的仓库</h2>
|
|
829
|
+
<span>${entries.length} 个</span>
|
|
830
|
+
</div>
|
|
831
|
+
<div class="grid">
|
|
832
|
+
${cards}
|
|
833
|
+
</div>
|
|
834
|
+
</section>
|
|
835
|
+
|
|
836
|
+
<footer>
|
|
837
|
+
由 <a href="https://github.com/shuxueshuxue/spexcode" target="_blank" rel="noopener noreferrer">SpexCode</a> 构建。
|
|
838
|
+
<a href="https://spexcode.net/zh/flatcode/">文档</a>。
|
|
839
|
+
本站仅显示已提交的 spec,不显示会话,也不提供写入功能。
|
|
840
|
+
</footer>
|
|
841
|
+
</div>
|
|
842
|
+
<script>
|
|
843
|
+
var command = document.getElementById('cmd')
|
|
844
|
+
var agent = document.getElementById('agent')
|
|
845
|
+
agent.addEventListener('change', function () {
|
|
846
|
+
command.textContent = 'spex flat new https://github.com/owner/repo --launcher ' + agent.value
|
|
847
|
+
})
|
|
848
|
+
document.getElementById('copy').addEventListener('click', function () {
|
|
849
|
+
var text = command.textContent
|
|
850
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text)
|
|
851
|
+
})
|
|
852
|
+
</script>
|
|
853
|
+
</body>
|
|
854
|
+
</html>
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
export async function flatGallery(out, flatDirs, log = console.log) {
|
|
858
|
+
if (!flatDirs.length)
|
|
859
|
+
throw new Error('spex flat gallery: name at least one flat directory');
|
|
860
|
+
const target = resolve(out);
|
|
861
|
+
mkdirSync(target, { recursive: true });
|
|
862
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
863
|
+
const entries = [];
|
|
864
|
+
const receipts = [];
|
|
865
|
+
for (const dir of flatDirs) {
|
|
866
|
+
const flat = resolve(dir);
|
|
867
|
+
const recordPath = join(flat, 'flat.json');
|
|
868
|
+
if (!existsSync(recordPath))
|
|
869
|
+
throw new Error(`spex flat gallery: ${flat} is not a flat — no flat.json`);
|
|
870
|
+
const record = JSON.parse(readFileSync(recordPath, 'utf8'));
|
|
871
|
+
const site = join(flat, 'site');
|
|
872
|
+
if (!existsSync(join(site, 'index.html'))) {
|
|
873
|
+
throw new Error(`spex flat gallery: ${flat} has no site — run \`spex flat site ${dir}\` first`);
|
|
874
|
+
}
|
|
875
|
+
const slug = gallerySlug(record.source);
|
|
876
|
+
const dest = join(target, slug);
|
|
877
|
+
mkdirSync(dest, { recursive: true });
|
|
878
|
+
cpSync(site, dest, { recursive: true });
|
|
879
|
+
const graph = JSON.parse(readFileSync(join(dest, 'public-graph.json'), 'utf8'));
|
|
880
|
+
entries.push({
|
|
881
|
+
slug,
|
|
882
|
+
source: record.source,
|
|
883
|
+
revision: record.revision,
|
|
884
|
+
coverage: record.gate?.coverage ?? 0,
|
|
885
|
+
governed: record.gate?.governed ?? 0,
|
|
886
|
+
nodes: graph.nodes.length,
|
|
887
|
+
passed: record.passed === true,
|
|
888
|
+
languages: record.profile?.languages ?? [],
|
|
889
|
+
lang: record.lang ?? 'en',
|
|
890
|
+
});
|
|
891
|
+
const release = join(dest, 'public-spec-release.json');
|
|
892
|
+
receipts.push({ slug, release: `${slug}/public-spec-release.json`, sha256: sha256(readFileSync(release)) });
|
|
893
|
+
log(` ${slug} — ${graph.nodes.length} nodes, ${record.gate?.coverage ?? 0}% of ${record.gate?.governed ?? 0} governed`);
|
|
894
|
+
}
|
|
895
|
+
// @@@ order - a tree written in the reader's own language goes first. The site defaults to Chinese, so a
|
|
896
|
+
// Chinese-prose flat is the one that demonstrates the product to the visitor it is aimed at; an English tree
|
|
897
|
+
// shown first makes the whole page look like a translation of somebody else's thing.
|
|
898
|
+
const rank = (entry) => (entry.lang.toLowerCase().startsWith('zh') ? 0 : entry.lang.toLowerCase() === 'en' ? 2 : 1);
|
|
899
|
+
entries.sort((a, b) => rank(a) - rank(b) || a.slug.localeCompare(b.slug));
|
|
900
|
+
receipts.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
901
|
+
copyFileSync(join(PKG, 'src', 'flatcode-banner.webp'), join(target, 'flatcode-banner.webp'));
|
|
902
|
+
writeFileSync(join(target, 'index.html'), galleryIndexHtml(entries));
|
|
903
|
+
// The manifest is what makes a publish auditable: it names every entry and hashes each flat's own release
|
|
904
|
+
// manifest, so what landed on a host can be compared with what was built without trusting the transport.
|
|
905
|
+
writeFileSync(join(target, 'gallery.json'), `${JSON.stringify({
|
|
906
|
+
schema: 'spexcode.flat-gallery/v1',
|
|
907
|
+
entries: entries.map((entry) => ({ ...entry })),
|
|
908
|
+
releases: receipts,
|
|
909
|
+
}, null, 2)}\n`);
|
|
910
|
+
log(`gallery: ${entries.length} flat(s) → ${target}`);
|
|
911
|
+
return entries;
|
|
912
|
+
}
|
|
913
|
+
export async function runFlat(argv) {
|
|
914
|
+
const sub = argv[0];
|
|
915
|
+
const { commandHelp } = await import('./help.js');
|
|
916
|
+
if (sub === undefined || sub === '--help' || sub === '-h') {
|
|
917
|
+
console.log(commandHelp('flat'));
|
|
918
|
+
return sub === undefined ? 0 : 0;
|
|
919
|
+
}
|
|
920
|
+
if (sub === 'site') {
|
|
921
|
+
const dir = argv[1];
|
|
922
|
+
if (!dir || argv.length !== 2) {
|
|
923
|
+
console.error('usage: spex flat site <flat-dir>');
|
|
924
|
+
return 2;
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
await flatSite(dir);
|
|
928
|
+
return 0;
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
console.error(error.message);
|
|
932
|
+
return 1;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (sub === 'gallery') {
|
|
936
|
+
const rest = argv.slice(1);
|
|
937
|
+
const at = rest.indexOf('--out');
|
|
938
|
+
const out = at >= 0 ? rest[at + 1] : undefined;
|
|
939
|
+
const dirs = rest.filter((token, index) => token !== '--out' && !(at >= 0 && index === at + 1));
|
|
940
|
+
if (!out || !dirs.length) {
|
|
941
|
+
console.error('usage: spex flat gallery --out <dir> <flat-dir>…');
|
|
942
|
+
return 2;
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
await flatGallery(out, dirs);
|
|
946
|
+
return 0;
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
console.error(error.message);
|
|
950
|
+
return 1;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (sub !== 'new') {
|
|
954
|
+
console.error(`spex flat: unknown subcommand "${sub}". Run \`spex flat\` for the command map.`);
|
|
955
|
+
return 2;
|
|
956
|
+
}
|
|
957
|
+
const known = new Set(['out', 'launcher', 'rounds', 'coverage', 'lang']);
|
|
958
|
+
const flags = new Map();
|
|
959
|
+
const positional = [];
|
|
960
|
+
const rest = argv.slice(1);
|
|
961
|
+
for (let at = 0; at < rest.length; at += 1) {
|
|
962
|
+
const token = rest[at];
|
|
963
|
+
if (!token.startsWith('--')) {
|
|
964
|
+
positional.push(token);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const name = token.slice(2);
|
|
968
|
+
if (!known.has(name)) {
|
|
969
|
+
console.error(`spex flat new: unknown flag --${name} (known: ${[...known].map((k) => `--${k}`).join(', ')})`);
|
|
970
|
+
return 2;
|
|
971
|
+
}
|
|
972
|
+
const value = rest[at + 1];
|
|
973
|
+
if (value === undefined || value.startsWith('--')) {
|
|
974
|
+
console.error(`spex flat new: --${name} expects a value`);
|
|
975
|
+
return 2;
|
|
976
|
+
}
|
|
977
|
+
flags.set(name, value);
|
|
978
|
+
at += 1;
|
|
979
|
+
}
|
|
980
|
+
if (positional.length !== 1) {
|
|
981
|
+
console.error('usage: spex flat new <repo-url|path> [--out <dir>] [--launcher <name>] [--rounds <n>] [--coverage <pct>]');
|
|
982
|
+
return 2;
|
|
983
|
+
}
|
|
984
|
+
const number = (name) => {
|
|
985
|
+
const raw = flags.get(name);
|
|
986
|
+
if (raw === undefined)
|
|
987
|
+
return undefined;
|
|
988
|
+
const value = Number(raw);
|
|
989
|
+
if (!Number.isFinite(value))
|
|
990
|
+
throw new Error(`spex flat: --${name} expects a number, got ${JSON.stringify(raw)}`);
|
|
991
|
+
return value;
|
|
992
|
+
};
|
|
993
|
+
try {
|
|
994
|
+
const result = await flatNew({
|
|
995
|
+
target: positional[0],
|
|
996
|
+
out: flags.get('out'),
|
|
997
|
+
launcher: flags.get('launcher'),
|
|
998
|
+
rounds: number('rounds'),
|
|
999
|
+
coverage: number('coverage'),
|
|
1000
|
+
lang: flags.get('lang'),
|
|
1001
|
+
});
|
|
1002
|
+
const { gate } = result;
|
|
1003
|
+
console.log('');
|
|
1004
|
+
console.log(`${result.passed ? 'converged' : 'PARTIAL'} after ${result.rounds} round(s)`);
|
|
1005
|
+
console.log(` coverage ${gate.coverage}% of ${gate.governed} governed source files`);
|
|
1006
|
+
console.log(` lint ${gate.errors} error(s)`);
|
|
1007
|
+
console.log(` flat ${result.out}`);
|
|
1008
|
+
if (!result.passed) {
|
|
1009
|
+
// A partial flat reports what still fails and exits non-zero. Reporting a converged tree we did not
|
|
1010
|
+
// measure would make the gate decorative.
|
|
1011
|
+
console.log('');
|
|
1012
|
+
console.log('still failing:');
|
|
1013
|
+
for (const finding of gate.errorFindings.slice(0, 10))
|
|
1014
|
+
console.log(` ${finding}`);
|
|
1015
|
+
if (gate.uncovered)
|
|
1016
|
+
console.log(` ${gate.uncovered} source file(s) still unclaimed by any node`);
|
|
1017
|
+
console.log('');
|
|
1018
|
+
console.log('Raise --rounds to keep going, or lower --coverage if this repository has files no spec should claim.');
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
1021
|
+
return 0;
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
console.error(error.message);
|
|
1025
|
+
return 1;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async function gateOf(repo) {
|
|
1029
|
+
const lint = await spex(['spec', 'lint', '--json'], repo);
|
|
1030
|
+
if (!lint.stdout.trim())
|
|
1031
|
+
throw new Error(`spex flat: spec lint produced no report — ${lint.stderr.trim()}`);
|
|
1032
|
+
return readGate(lint.stdout);
|
|
1033
|
+
}
|
|
1034
|
+
// Flatcode's own commits bypass the lint hook on purpose: an intermediate round is EXPECTED to be incomplete,
|
|
1035
|
+
// and a hook that refused it would strand the round's work uncommitted where the next round cannot see it.
|
|
1036
|
+
// The gate reading, not the commit hook, is what decides whether this flat converged. The path list is also
|
|
1037
|
+
// the write boundary for an adopted local repository: Flatcode owns intent, never the source it describes.
|
|
1038
|
+
async function commit(repo, message, paths) {
|
|
1039
|
+
await git(['add', '--', ...paths], repo);
|
|
1040
|
+
const staged = await git(['diff', '--cached', '--quiet'], repo);
|
|
1041
|
+
if (staged.code === 0)
|
|
1042
|
+
return;
|
|
1043
|
+
const result = await run('git', ['-c', 'user.name=Flatcode', '-c', 'user.email=flatcode@spexcode.invalid', 'commit', '--quiet', '--no-verify', '-m', message], repo);
|
|
1044
|
+
if (result.code !== 0)
|
|
1045
|
+
throw new Error(`spex flat: committing "${message}" failed — ${result.stderr.trim()}`);
|
|
1046
|
+
}
|
|
1047
|
+
async function assertOnlyFlatChanges(repo, before, beforeUntracked) {
|
|
1048
|
+
const readings = await Promise.all([
|
|
1049
|
+
gitOrThrow(['diff', '--name-only', `${before}..`], repo, 'checking committed agent changes'),
|
|
1050
|
+
gitOrThrow(['diff', '--name-only'], repo, 'checking unstaged agent changes'),
|
|
1051
|
+
gitOrThrow(['diff', '--cached', '--name-only'], repo, 'checking staged agent changes'),
|
|
1052
|
+
gitOrThrow(['ls-files', '--others', '--exclude-standard'], repo, 'checking untracked agent changes'),
|
|
1053
|
+
]);
|
|
1054
|
+
const changed = [...new Set(readings.flatMap((reading, index) => reading.split('\n').filter((path) => Boolean(path) && (index !== 3 || !beforeUntracked.has(path)))))];
|
|
1055
|
+
const forbidden = changed.filter((path) => path !== '.spec' && !path.startsWith('.spec/'));
|
|
1056
|
+
if (forbidden.length) {
|
|
1057
|
+
// The repository was clean before the round. Resetting to that exact revision leaves the agent's .spec
|
|
1058
|
+
// edits in the working tree, then restoring only baseline non-spec paths erases every source/config edit
|
|
1059
|
+
// the agent made, including ones it committed itself. New non-spec paths did not exist at the boundary and
|
|
1060
|
+
// are removed by their exact git-reported names. A rejected round therefore cannot leave source changes
|
|
1061
|
+
// behind under the misleading name of a documentation run.
|
|
1062
|
+
const baseline = new Set((await gitOrThrow(['ls-tree', '-r', '--name-only', before], repo, 'loading the round baseline')).split('\n').filter(Boolean));
|
|
1063
|
+
await gitOrThrow(['reset', '--mixed', before], repo, 'discarding the agent commit');
|
|
1064
|
+
for (const path of forbidden) {
|
|
1065
|
+
if (baseline.has(path)) {
|
|
1066
|
+
await gitOrThrow(['checkout', before, '--', path], repo, `restoring ${path}`);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const target = resolve(repo, path);
|
|
1070
|
+
if (!target.startsWith(`${repo}${sep}`))
|
|
1071
|
+
throw new Error(`spex flat: invalid agent path ${JSON.stringify(path)}`);
|
|
1072
|
+
rmSync(target, { force: true, recursive: true });
|
|
1073
|
+
}
|
|
1074
|
+
throw new Error(`spex flat: the agent changed files outside .spec (${forbidden.join(', ')}). Flatcode discarded those changes and will not commit them.`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|