@quolu/lattice 0.1.0 → 0.12.2
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 +87 -4
- package/bin/lattice-bridge.mjs +87 -0
- package/bin/lattice-dashboard.mjs +70 -0
- package/bin/lattice-mcp.mjs +9 -9
- package/bin/lattice.mjs +61 -8
- package/docs/bridge-setup.md +87 -0
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -0
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -0
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -0
- package/package.json +19 -7
- package/sensor/dist/bin/command-supervision.d.ts +1 -1
- package/sensor/dist/bin/command-supervision.js +4 -4
- package/sensor/dist/bin/command-supervision.js.map +1 -1
- package/sensor/dist/bin/fatal-handler.js +2 -2
- package/sensor/dist/bin/fatal-handler.js.map +1 -1
- package/sensor/dist/bin/{codegraph.js → lattice-sensor.js} +207 -328
- package/sensor/dist/bin/node-version-check.d.ts +7 -5
- package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
- package/sensor/dist/bin/node-version-check.js +19 -14
- package/sensor/dist/bin/node-version-check.js.map +1 -1
- package/sensor/dist/context/index.d.ts +1 -1
- package/sensor/dist/context/index.js +6 -6
- package/sensor/dist/context/index.js.map +1 -1
- package/sensor/dist/db/index.d.ts +5 -5
- package/sensor/dist/db/index.d.ts.map +1 -1
- package/sensor/dist/db/index.js +7 -7
- package/sensor/dist/db/index.js.map +1 -1
- package/sensor/dist/db/migrations.js +1 -1
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +1 -1
- package/sensor/dist/db/queries.js +5 -5
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +2 -2
- package/sensor/dist/db/sqlite-adapter.d.ts +2 -2
- package/sensor/dist/db/sqlite-adapter.js +3 -3
- package/sensor/dist/db/sqlite-adapter.js.map +1 -1
- package/sensor/dist/db/wal-valve.d.ts +1 -1
- package/sensor/dist/db/wal-valve.d.ts.map +1 -1
- package/sensor/dist/db/wal-valve.js +2 -2
- package/sensor/dist/db/wal-valve.js.map +1 -1
- package/sensor/dist/directory.d.ts +27 -63
- package/sensor/dist/directory.d.ts.map +1 -1
- package/sensor/dist/directory.js +78 -134
- package/sensor/dist/directory.js.map +1 -1
- package/sensor/dist/errors.d.ts +15 -15
- package/sensor/dist/errors.d.ts.map +1 -1
- package/sensor/dist/errors.js +21 -21
- package/sensor/dist/errors.js.map +1 -1
- package/sensor/dist/extraction/dynamic-import.d.ts +1 -1
- package/sensor/dist/extraction/dynamic-import.js +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.js +1 -1
- package/sensor/dist/extraction/generated-detection.d.ts +1 -1
- package/sensor/dist/extraction/generated-detection.js +1 -1
- package/sensor/dist/extraction/grammars.d.ts +3 -3
- package/sensor/dist/extraction/grammars.js +5 -5
- package/sensor/dist/extraction/grammars.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +6 -6
- package/sensor/dist/extraction/index.js +50 -50
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/kernel/decode.d.ts +1 -1
- package/sensor/dist/extraction/kernel/decode.js +1 -1
- package/sensor/dist/extraction/kernel/index.d.ts +3 -3
- package/sensor/dist/extraction/kernel/index.js +6 -6
- package/sensor/dist/extraction/kernel/index.js.map +1 -1
- package/sensor/dist/extraction/kernel/layout.d.ts +1 -1
- package/sensor/dist/extraction/kernel/layout.js +1 -1
- package/sensor/dist/extraction/kernel/loader.d.ts +6 -6
- package/sensor/dist/extraction/kernel/loader.js +13 -13
- package/sensor/dist/extraction/kernel/loader.js.map +1 -1
- package/sensor/dist/extraction/languages/cfquery.d.ts +1 -1
- package/sensor/dist/extraction/languages/cfquery.js +1 -1
- package/sensor/dist/extraction/languages/lua.d.ts +11 -0
- package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/lua.js +1 -0
- package/sensor/dist/extraction/languages/lua.js.map +1 -1
- package/sensor/dist/extraction/parse-pool.d.ts +5 -5
- package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
- package/sensor/dist/extraction/parse-pool.js +4 -4
- package/sensor/dist/extraction/parse-worker.js +1 -1
- package/sensor/dist/extraction/parse-worker.js.map +1 -1
- package/sensor/dist/extraction/store-writer.d.ts +1 -1
- package/sensor/dist/extraction/store-writer.js +1 -1
- package/sensor/dist/extraction/svelte-extractor.d.ts +1 -1
- package/sensor/dist/extraction/svelte-extractor.js +1 -1
- package/sensor/dist/extraction/tree-sitter-helpers.js +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts +2 -2
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +71 -4
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/extraction/wasm-runtime-flags.d.ts +2 -2
- package/sensor/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/sensor/dist/extraction/wasm-runtime-flags.js +5 -5
- package/sensor/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/sensor/dist/index.d.ts +36 -36
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +71 -71
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/mcp/daemon-manager.js +1 -1
- package/sensor/dist/mcp/daemon-paths.d.ts +3 -3
- package/sensor/dist/mcp/daemon-paths.js +7 -7
- package/sensor/dist/mcp/daemon-paths.js.map +1 -1
- package/sensor/dist/mcp/daemon-registry.d.ts +2 -2
- package/sensor/dist/mcp/daemon-registry.js +5 -5
- package/sensor/dist/mcp/daemon.d.ts +7 -7
- package/sensor/dist/mcp/daemon.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon.js +21 -21
- package/sensor/dist/mcp/daemon.js.map +1 -1
- package/sensor/dist/mcp/dynamic-boundaries.js +1 -1
- package/sensor/dist/mcp/engine.d.ts +9 -9
- package/sensor/dist/mcp/engine.d.ts.map +1 -1
- package/sensor/dist/mcp/engine.js +44 -44
- package/sensor/dist/mcp/engine.js.map +1 -1
- package/sensor/dist/mcp/index.d.ts +14 -14
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +41 -41
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.js +7 -7
- package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
- package/sensor/dist/mcp/ppid-watchdog.d.ts +3 -3
- package/sensor/dist/mcp/ppid-watchdog.js +2 -2
- package/sensor/dist/mcp/proxy.d.ts.map +1 -1
- package/sensor/dist/mcp/proxy.js +37 -37
- package/sensor/dist/mcp/proxy.js.map +1 -1
- package/sensor/dist/mcp/query-pool.d.ts +3 -3
- package/sensor/dist/mcp/query-pool.js +7 -7
- package/sensor/dist/mcp/query-pool.js.map +1 -1
- package/sensor/dist/mcp/query-worker.d.ts +2 -2
- package/sensor/dist/mcp/query-worker.js +7 -7
- package/sensor/dist/mcp/query-worker.js.map +1 -1
- package/sensor/dist/mcp/server-instructions.d.ts +10 -12
- package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
- package/sensor/dist/mcp/server-instructions.js +39 -37
- package/sensor/dist/mcp/server-instructions.js.map +1 -1
- package/sensor/dist/mcp/session.d.ts +2 -2
- package/sensor/dist/mcp/session.js +18 -18
- package/sensor/dist/mcp/session.js.map +1 -1
- package/sensor/dist/mcp/startup-handshake.d.ts +3 -3
- package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
- package/sensor/dist/mcp/startup-handshake.js +3 -3
- package/sensor/dist/mcp/startup-handshake.js.map +1 -1
- package/sensor/dist/mcp/tools.d.ts +53 -53
- package/sensor/dist/mcp/tools.d.ts.map +1 -1
- package/sensor/dist/mcp/tools.js +180 -216
- package/sensor/dist/mcp/tools.js.map +1 -1
- package/sensor/dist/mcp/transport.d.ts +1 -1
- package/sensor/dist/mcp/transport.js +6 -6
- package/sensor/dist/mcp/transport.js.map +1 -1
- package/sensor/dist/mcp/version.d.ts +2 -2
- package/sensor/dist/mcp/version.d.ts.map +1 -1
- package/sensor/dist/mcp/version.js +3 -3
- package/sensor/dist/mcp/version.js.map +1 -1
- package/sensor/dist/project-config.d.ts +6 -6
- package/sensor/dist/project-config.d.ts.map +1 -1
- package/sensor/dist/project-config.js +10 -10
- package/sensor/dist/project-config.js.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.js +4 -4
- package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/frameworks/drupal.d.ts +3 -3
- package/sensor/dist/resolution/frameworks/drupal.js +3 -3
- package/sensor/dist/resolution/frameworks/fabric.js +1 -1
- package/sensor/dist/resolution/frameworks/fabric.js.map +1 -1
- package/sensor/dist/resolution/frameworks/java.js +1 -1
- package/sensor/dist/resolution/frameworks/java.js.map +1 -1
- package/sensor/dist/resolution/frameworks/svelte.js +1 -1
- package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
- package/sensor/dist/resolution/goframe-synthesizer.d.ts +1 -1
- package/sensor/dist/resolution/goframe-synthesizer.js +1 -1
- package/sensor/dist/resolution/index.d.ts +1 -1
- package/sensor/dist/resolution/index.js +9 -9
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/name-matcher.js +2 -2
- package/sensor/dist/resolution/name-matcher.js.map +1 -1
- package/sensor/dist/resolution/resolver-pool.d.ts +2 -2
- package/sensor/dist/resolution/resolver-pool.js +4 -4
- package/sensor/dist/resolution/resolver-pool.js.map +1 -1
- package/sensor/dist/resolution/resolver-worker.js +2 -2
- package/sensor/dist/resolution/resolver-worker.js.map +1 -1
- package/sensor/dist/resolution/workspace-packages.js +1 -1
- package/sensor/dist/resolution/workspace-packages.js.map +1 -1
- package/sensor/dist/search/identifier-segments.d.ts +1 -1
- package/sensor/dist/search/identifier-segments.js +1 -1
- package/sensor/dist/search/query-utils.d.ts +1 -0
- package/sensor/dist/search/query-utils.d.ts.map +1 -1
- package/sensor/dist/search/query-utils.js +10 -3
- package/sensor/dist/search/query-utils.js.map +1 -1
- package/sensor/dist/sync/git-hooks.d.ts +9 -9
- package/sensor/dist/sync/git-hooks.d.ts.map +1 -1
- package/sensor/dist/sync/git-hooks.js +15 -15
- package/sensor/dist/sync/git-hooks.js.map +1 -1
- package/sensor/dist/sync/watch-policy.d.ts +2 -2
- package/sensor/dist/sync/watch-policy.js +5 -5
- package/sensor/dist/sync/watch-policy.js.map +1 -1
- package/sensor/dist/sync/watcher.d.ts +2 -2
- package/sensor/dist/sync/watcher.d.ts.map +1 -1
- package/sensor/dist/sync/watcher.js +16 -16
- package/sensor/dist/sync/watcher.js.map +1 -1
- package/sensor/dist/sync/worktree.d.ts +5 -5
- package/sensor/dist/sync/worktree.d.ts.map +1 -1
- package/sensor/dist/sync/worktree.js +9 -9
- package/sensor/dist/sync/worktree.js.map +1 -1
- package/sensor/dist/telemetry/index.d.ts +8 -8
- package/sensor/dist/telemetry/index.d.ts.map +1 -1
- package/sensor/dist/telemetry/index.js +16 -16
- package/sensor/dist/telemetry/index.js.map +1 -1
- package/sensor/dist/types.d.ts +2 -2
- package/sensor/dist/types.js +1 -1
- package/sensor/dist/ui/color.d.ts +2 -2
- package/sensor/dist/ui/color.js +2 -2
- package/sensor/dist/ui/glyphs.d.ts +3 -3
- package/sensor/dist/ui/glyphs.js +7 -7
- package/sensor/dist/ui/glyphs.js.map +1 -1
- package/sensor/dist/utils.d.ts +4 -4
- package/sensor/dist/utils.js +8 -8
- package/sensor/dist/utils.js.map +1 -1
- package/sensor/package.json +8 -13
- package/src/artifact-contracts.mjs +2 -2
- package/src/boundary-compiler.mjs +28 -28
- package/src/boundary-observation-compiler-v2.mjs +10 -10
- package/src/bounded-seam.mjs +144 -0
- package/src/bridge-cli.mjs +227 -0
- package/src/bridge-config.mjs +346 -0
- package/src/bridge-daemon.mjs +328 -0
- package/src/bridge-launch-agent.mjs +311 -0
- package/src/bridge-server.mjs +309 -0
- package/src/cli-help.mjs +163 -0
- package/src/control-compiler.mjs +30 -30
- package/src/dag-chain.mjs +261 -0
- package/src/factory-diagnostics.mjs +8 -7
- package/src/hash-chain.mjs +76 -0
- package/src/node-version-guard.mjs +5 -4
- package/src/project-cli.mjs +392 -0
- package/src/project-identity.mjs +71 -0
- package/src/rc1-evidence-bundle.mjs +16 -16
- package/src/rc1-v4-campaign.mjs +15 -14
- package/src/rc1-v5-artifact-set.mjs +2 -2
- package/src/rc1-v5-campaign.mjs +15 -14
- package/src/rc1-v6-artifact-set.mjs +18 -18
- package/src/rc1-v6-campaign.mjs +16 -16
- package/src/rc1-v6-causal-binding.mjs +16 -14
- package/src/rc1-v6-measurement.mjs +20 -38
- package/src/rc2-artifact-set.mjs +45 -40
- package/src/rc2-campaign.mjs +48 -66
- package/src/rc2-delivery-policy-front-end.mjs +30 -30
- package/src/rc2-rc1-transfer-front-end.mjs +5 -5
- package/src/rc3-actual-dogfood.mjs +6 -6
- package/src/rc3-dogfood-scaffold.mjs +12 -7
- package/src/rc3-scripted-campaign.mjs +75 -21
- package/src/rc4-stage1-dogfood.mjs +24 -7
- package/src/runtime-cli.mjs +2424 -45
- package/src/runtime-contracts.mjs +7 -7
- package/src/runtime-control-store.mjs +547 -0
- package/src/runtime-controller-protocol.mjs +517 -0
- package/src/runtime-direct-os-observer.mjs +284 -0
- package/src/runtime-engine.mjs +2 -2
- package/src/runtime-errors.mjs +1 -1
- package/src/runtime-event-store.mjs +15 -43
- package/src/runtime-front-end.mjs +26 -26
- package/src/runtime-gate-store.mjs +477 -0
- package/src/runtime-hold-recompile.mjs +214 -23
- package/src/runtime-lifecycle-lock.mjs +290 -0
- package/src/runtime-managed-supervisor.mjs +1276 -0
- package/src/runtime-multi-epoch-store.mjs +761 -0
- package/src/runtime-projection.mjs +62 -0
- package/src/seam-transform.mjs +38 -38
- package/src/{codegraph-adapter.mjs → sensor-adapter.mjs} +7 -7
- package/src/sensor-cli.mjs +107 -0
- package/src/sensor-runtime.mjs +48 -0
- package/src/timestamp-contract.mjs +8 -0
- package/src/todo-chain.mjs +178 -0
- package/src/todo-cli.mjs +1032 -0
- package/src/todo-contracts.mjs +501 -0
- package/src/todo-dashboard-registry.mjs +318 -0
- package/src/todo-gantt-html.mjs +416 -0
- package/src/todo-gantt-layout.mjs +603 -0
- package/src/todo-gantt-live.mjs +269 -0
- package/src/todo-gantt-presentation.mjs +217 -0
- package/src/todo-gantt-svg.mjs +249 -0
- package/src/todo-markdown-renderer.mjs +260 -0
- package/src/todo-migration.mjs +260 -0
- package/src/todo-narrative-anchor.mjs +130 -0
- package/src/todo-revision.mjs +425 -0
- package/src/todo-status.mjs +340 -0
- package/src/todo-store.mjs +3451 -0
- package/src/treatment-compiler.mjs +28 -28
- package/src/treatment-runner.mjs +42 -44
- package/sensor/dist/bin/codegraph.d.ts +0 -26
- package/sensor/dist/bin/codegraph.d.ts.map +0 -1
- package/sensor/dist/bin/codegraph.js.map +0 -1
- package/sensor/dist/bin/uninstall.d.ts +0 -14
- package/sensor/dist/bin/uninstall.d.ts.map +0 -1
- package/sensor/dist/bin/uninstall.js +0 -36
- package/sensor/dist/bin/uninstall.js.map +0 -1
- package/sensor/dist/installer/beta-signup.d.ts +0 -54
- package/sensor/dist/installer/beta-signup.d.ts.map +0 -1
- package/sensor/dist/installer/beta-signup.js +0 -178
- package/sensor/dist/installer/beta-signup.js.map +0 -1
- package/sensor/dist/installer/config-writer.d.ts +0 -28
- package/sensor/dist/installer/config-writer.d.ts.map +0 -1
- package/sensor/dist/installer/config-writer.js +0 -91
- package/sensor/dist/installer/config-writer.js.map +0 -1
- package/sensor/dist/installer/index.d.ts +0 -142
- package/sensor/dist/installer/index.d.ts.map +0 -1
- package/sensor/dist/installer/index.js +0 -622
- package/sensor/dist/installer/index.js.map +0 -1
- package/sensor/dist/installer/instructions-template.d.ts +0 -41
- package/sensor/dist/installer/instructions-template.d.ts.map +0 -1
- package/sensor/dist/installer/instructions-template.js +0 -53
- package/sensor/dist/installer/instructions-template.js.map +0 -1
- package/sensor/dist/installer/targets/antigravity.d.ts +0 -57
- package/sensor/dist/installer/targets/antigravity.d.ts.map +0 -1
- package/sensor/dist/installer/targets/antigravity.js +0 -308
- package/sensor/dist/installer/targets/antigravity.js.map +0 -1
- package/sensor/dist/installer/targets/claude.d.ts +0 -62
- package/sensor/dist/installer/targets/claude.d.ts.map +0 -1
- package/sensor/dist/installer/targets/claude.js +0 -454
- package/sensor/dist/installer/targets/claude.js.map +0 -1
- package/sensor/dist/installer/targets/codex.d.ts +0 -18
- package/sensor/dist/installer/targets/codex.d.ts.map +0 -1
- package/sensor/dist/installer/targets/codex.js +0 -185
- package/sensor/dist/installer/targets/codex.js.map +0 -1
- package/sensor/dist/installer/targets/cursor.d.ts +0 -35
- package/sensor/dist/installer/targets/cursor.d.ts.map +0 -1
- package/sensor/dist/installer/targets/cursor.js +0 -254
- package/sensor/dist/installer/targets/cursor.js.map +0 -1
- package/sensor/dist/installer/targets/gemini.d.ts +0 -26
- package/sensor/dist/installer/targets/gemini.d.ts.map +0 -1
- package/sensor/dist/installer/targets/gemini.js +0 -165
- package/sensor/dist/installer/targets/gemini.js.map +0 -1
- package/sensor/dist/installer/targets/hermes.d.ts +0 -18
- package/sensor/dist/installer/targets/hermes.d.ts.map +0 -1
- package/sensor/dist/installer/targets/hermes.js +0 -359
- package/sensor/dist/installer/targets/hermes.js.map +0 -1
- package/sensor/dist/installer/targets/kiro.d.ts +0 -27
- package/sensor/dist/installer/targets/kiro.d.ts.map +0 -1
- package/sensor/dist/installer/targets/kiro.js +0 -178
- package/sensor/dist/installer/targets/kiro.js.map +0 -1
- package/sensor/dist/installer/targets/opencode.d.ts +0 -38
- package/sensor/dist/installer/targets/opencode.d.ts.map +0 -1
- package/sensor/dist/installer/targets/opencode.js +0 -288
- package/sensor/dist/installer/targets/opencode.js.map +0 -1
- package/sensor/dist/installer/targets/registry.d.ts +0 -35
- package/sensor/dist/installer/targets/registry.d.ts.map +0 -1
- package/sensor/dist/installer/targets/registry.js +0 -91
- package/sensor/dist/installer/targets/registry.js.map +0 -1
- package/sensor/dist/installer/targets/shared.d.ts +0 -101
- package/sensor/dist/installer/targets/shared.d.ts.map +0 -1
- package/sensor/dist/installer/targets/shared.js +0 -264
- package/sensor/dist/installer/targets/shared.js.map +0 -1
- package/sensor/dist/installer/targets/toml.d.ts +0 -52
- package/sensor/dist/installer/targets/toml.d.ts.map +0 -1
- package/sensor/dist/installer/targets/toml.js +0 -147
- package/sensor/dist/installer/targets/toml.js.map +0 -1
- package/sensor/dist/installer/targets/types.d.ts +0 -108
- package/sensor/dist/installer/targets/types.d.ts.map +0 -1
- package/sensor/dist/installer/targets/types.js +0 -16
- package/sensor/dist/installer/targets/types.js.map +0 -1
- package/sensor/dist/upgrade/index.d.ts +0 -171
- package/sensor/dist/upgrade/index.d.ts.map +0 -1
- package/sensor/dist/upgrade/index.js +0 -659
- package/sensor/dist/upgrade/index.js.map +0 -1
- package/sensor/dist/upgrade/remove-binary.d.ts +0 -87
- package/sensor/dist/upgrade/remove-binary.d.ts.map +0 -1
- package/sensor/dist/upgrade/remove-binary.js +0 -289
- package/sensor/dist/upgrade/remove-binary.js.map +0 -1
- package/sensor/dist/upgrade/update-check.d.ts +0 -92
- package/sensor/dist/upgrade/update-check.d.ts.map +0 -1
- package/sensor/dist/upgrade/update-check.js +0 -258
- package/sensor/dist/upgrade/update-check.js.map +0 -1
- package/src/bootstrap.mjs +0 -88
package/src/todo-cli.mjs
ADDED
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
lstat, mkdir, open, readFile, realpath, rename, rm,
|
|
5
|
+
} from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { parseTree } from 'jsonc-parser';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
canonicalizeTodoArtifact,
|
|
11
|
+
digestTodoArtifact,
|
|
12
|
+
exactRecord,
|
|
13
|
+
isTodoDigest,
|
|
14
|
+
isTodoIdentifier,
|
|
15
|
+
isTodoRef,
|
|
16
|
+
todoSelfDigest,
|
|
17
|
+
validateEvidenceDescriptor,
|
|
18
|
+
} from './todo-contracts.mjs';
|
|
19
|
+
import { projectTodoChainV1 } from './todo-chain.mjs';
|
|
20
|
+
import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
|
|
21
|
+
import { resolveProjectIdentity } from './project-identity.mjs';
|
|
22
|
+
import { layoutTodoGantt } from './todo-gantt-layout.mjs';
|
|
23
|
+
import { loadTodoGanttPresentation } from './todo-gantt-presentation.mjs';
|
|
24
|
+
import { startTodoGanttLiveServer } from './todo-gantt-live.mjs';
|
|
25
|
+
import {
|
|
26
|
+
renderTodoGanttHtml,
|
|
27
|
+
TODO_GANTT_HTML_MAX_BYTES,
|
|
28
|
+
TODO_GANTT_RENDERER_VERSION,
|
|
29
|
+
} from './todo-gantt-html.mjs';
|
|
30
|
+
import { verifyNarrativeAnchors } from './todo-narrative-anchor.mjs';
|
|
31
|
+
import {
|
|
32
|
+
appendTodoEvent,
|
|
33
|
+
applyPhaseTodoRevision,
|
|
34
|
+
applyTodoRevision,
|
|
35
|
+
applyTodoRevisionSet,
|
|
36
|
+
createTodoStoreWriter,
|
|
37
|
+
TodoStoreError,
|
|
38
|
+
readTodoStore,
|
|
39
|
+
readTodoStoreStable,
|
|
40
|
+
rebuildTodoSnapshot,
|
|
41
|
+
verifyPhaseTodoRevisionSources,
|
|
42
|
+
verifyTodoRevisionSources,
|
|
43
|
+
} from './todo-store.mjs';
|
|
44
|
+
import {
|
|
45
|
+
appendTodoExtraction,
|
|
46
|
+
validateTodoExtraction,
|
|
47
|
+
} from './todo-migration.mjs';
|
|
48
|
+
import { projectTodoStatus } from './todo-status.mjs';
|
|
49
|
+
import {
|
|
50
|
+
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
51
|
+
validateTodoRevision, validateTodoRevisionSet,
|
|
52
|
+
} from './todo-revision.mjs';
|
|
53
|
+
|
|
54
|
+
const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
55
|
+
const DEFAULT_GANTT_REF = '.lattice/generated/gantt.html';
|
|
56
|
+
const GANTT_DESCRIPTOR_SUFFIX = '.status.json';
|
|
57
|
+
const MAX_GANTT_DESCRIPTOR_BYTES = 65_536;
|
|
58
|
+
const MAX_MIGRATION_INPUT_BYTES = 8_388_608;
|
|
59
|
+
const ACTOR_ENV_KEYS = Object.freeze([
|
|
60
|
+
'LATTICE_TODO_ACTOR_HOST',
|
|
61
|
+
'LATTICE_TODO_ACTOR_SESSION',
|
|
62
|
+
'LATTICE_TODO_ACTOR_AGENT',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function usageFailure(stderr, argv) {
|
|
66
|
+
const received = argv.length === 0 ? '(none)' : argv.join(' ').replace(/[\r\n]/gu, ' ');
|
|
67
|
+
stderr.write(`lattice todo: unsupported command or arguments: ${received}\n`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function typedFailure(stderr, error) {
|
|
72
|
+
const payload = {
|
|
73
|
+
schema: CLI_ERROR_SCHEMA,
|
|
74
|
+
code: error.code,
|
|
75
|
+
message: error.message,
|
|
76
|
+
};
|
|
77
|
+
if (error.detail !== null && typeof error.detail === 'object'
|
|
78
|
+
&& !Array.isArray(error.detail) && Object.keys(error.detail).length > 0) {
|
|
79
|
+
payload.detail = error.detail;
|
|
80
|
+
}
|
|
81
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveRepoRoot(cwd) {
|
|
86
|
+
try {
|
|
87
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
88
|
+
cwd,
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
91
|
+
}).trim();
|
|
92
|
+
} catch {
|
|
93
|
+
throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved', 'cwdのgit toplevelを解決できない');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function selectMembers(store, requestedPlanKey) {
|
|
98
|
+
if (requestedPlanKey === null) return store.members;
|
|
99
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === requestedPlanKey);
|
|
100
|
+
if (member === undefined) {
|
|
101
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
|
|
102
|
+
}
|
|
103
|
+
return [member];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function taskRef(plan, taskId) {
|
|
107
|
+
return { project_id: plan.project_id, plan_key: plan.plan_key, task_id: taskId };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function mergedTopology(store) {
|
|
111
|
+
return {
|
|
112
|
+
nodes: store.members.flatMap(({ plan }) => plan.tasks.map(({ task_id: taskId }) => taskRef(plan, taskId))),
|
|
113
|
+
hard_edges: store.members.flatMap(({ plan }) => plan.hard_dependencies),
|
|
114
|
+
joins: store.members.flatMap(({ plan }) => plan.joins),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function within(root, candidate) {
|
|
119
|
+
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasDuplicateJsonKey(node) {
|
|
123
|
+
if (node?.type === 'object') {
|
|
124
|
+
const keys = new Set();
|
|
125
|
+
for (const property of node.children ?? []) {
|
|
126
|
+
const [key, value] = property.children ?? [];
|
|
127
|
+
if (keys.has(key?.value) || hasDuplicateJsonKey(value)) return true;
|
|
128
|
+
keys.add(key?.value);
|
|
129
|
+
}
|
|
130
|
+
} else if (node?.type === 'array') {
|
|
131
|
+
return (node.children ?? []).some(hasDuplicateJsonKey);
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function readMigrationInput(repoRoot, inputRef) {
|
|
137
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
138
|
+
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
139
|
+
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
140
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined, { input_ref: inputRef });
|
|
141
|
+
}
|
|
142
|
+
let stats;
|
|
143
|
+
try { stats = await lstat(absolute); } catch {
|
|
144
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing', undefined, { input_ref: inputRef });
|
|
145
|
+
}
|
|
146
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
147
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_input_path', undefined, { input_ref: inputRef });
|
|
148
|
+
}
|
|
149
|
+
const resolved = await realpath(absolute);
|
|
150
|
+
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
151
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape', undefined, { input_ref: inputRef });
|
|
152
|
+
}
|
|
153
|
+
if (stats.size > MAX_MIGRATION_INPUT_BYTES) {
|
|
154
|
+
throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
155
|
+
}
|
|
156
|
+
const bytes = await readFile(resolved);
|
|
157
|
+
if (bytes.length > MAX_MIGRATION_INPUT_BYTES) {
|
|
158
|
+
throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
159
|
+
}
|
|
160
|
+
let text;
|
|
161
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch {
|
|
162
|
+
throw new TodoStoreError('INVALID_JSON', 'invalid_utf8');
|
|
163
|
+
}
|
|
164
|
+
const parseErrors = [];
|
|
165
|
+
const tree = parseTree(text, parseErrors, { allowTrailingComma: false, disallowComments: true });
|
|
166
|
+
if (parseErrors.length > 0 || tree === undefined) {
|
|
167
|
+
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
168
|
+
}
|
|
169
|
+
if (hasDuplicateJsonKey(tree)) throw new TodoStoreError('INVALID_JSON', 'duplicate_key');
|
|
170
|
+
let extraction;
|
|
171
|
+
try { extraction = JSON.parse(text); } catch {
|
|
172
|
+
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
173
|
+
}
|
|
174
|
+
if (!validateTodoExtraction(extraction)) {
|
|
175
|
+
throw new TodoStoreError('INVALID_TODO_EXTRACTION', 'schema_invalid');
|
|
176
|
+
}
|
|
177
|
+
return extraction;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readRevisionInput(repoRoot, inputRef, {
|
|
181
|
+
validate = validateTodoRevision,
|
|
182
|
+
invalidCode = 'REVISION_INVALID',
|
|
183
|
+
invalidReason = 'revision_schema_or_digest_invalid',
|
|
184
|
+
} = {}) {
|
|
185
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
186
|
+
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
187
|
+
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
188
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined, { input_ref: inputRef });
|
|
189
|
+
}
|
|
190
|
+
let stats;
|
|
191
|
+
try { stats = await lstat(absolute); } catch {
|
|
192
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing', undefined, { input_ref: inputRef });
|
|
193
|
+
}
|
|
194
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
195
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_input_path', undefined, { input_ref: inputRef });
|
|
196
|
+
}
|
|
197
|
+
const resolved = await realpath(absolute);
|
|
198
|
+
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
199
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape', undefined, { input_ref: inputRef });
|
|
200
|
+
}
|
|
201
|
+
if (stats.size > MAX_MIGRATION_INPUT_BYTES) throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
202
|
+
const bytes = await readFile(resolved);
|
|
203
|
+
if (bytes.length > MAX_MIGRATION_INPUT_BYTES) throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
204
|
+
let text;
|
|
205
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch {
|
|
206
|
+
throw new TodoStoreError('INVALID_JSON', 'invalid_utf8');
|
|
207
|
+
}
|
|
208
|
+
if (!text.endsWith('\n') || text.startsWith('\uFEFF') || text.includes('\r')
|
|
209
|
+
|| text.slice(0, -1).includes('\n')) {
|
|
210
|
+
throw new TodoStoreError(invalidCode, 'non_canonical_revision_bytes');
|
|
211
|
+
}
|
|
212
|
+
const parseErrors = [];
|
|
213
|
+
const tree = parseTree(text.slice(0, -1), parseErrors, { allowTrailingComma: false, disallowComments: true });
|
|
214
|
+
if (parseErrors.length > 0 || tree === undefined) throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
215
|
+
if (hasDuplicateJsonKey(tree)) throw new TodoStoreError('INVALID_JSON', 'duplicate_key');
|
|
216
|
+
let revision;
|
|
217
|
+
try { revision = JSON.parse(text.slice(0, -1)); } catch {
|
|
218
|
+
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
219
|
+
}
|
|
220
|
+
if (!validate(revision)) throw new TodoStoreError(invalidCode, invalidReason);
|
|
221
|
+
if (text !== `${canonicalizeTodoArtifact(revision)}\n`) {
|
|
222
|
+
throw new TodoStoreError(invalidCode, 'non_canonical_revision_bytes');
|
|
223
|
+
}
|
|
224
|
+
return revision;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function readEvidenceInput(repoRoot, inputRef) {
|
|
228
|
+
if (!isTodoRef(inputRef)) {
|
|
229
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined, { input_ref: inputRef });
|
|
230
|
+
}
|
|
231
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
232
|
+
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
233
|
+
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
234
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined, { input_ref: inputRef });
|
|
235
|
+
}
|
|
236
|
+
let stats;
|
|
237
|
+
try { stats = await lstat(absolute); } catch {
|
|
238
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing', undefined, { input_ref: inputRef });
|
|
239
|
+
}
|
|
240
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
241
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_input_path', undefined, { input_ref: inputRef });
|
|
242
|
+
}
|
|
243
|
+
const resolved = await realpath(absolute);
|
|
244
|
+
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
245
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape', undefined, { input_ref: inputRef });
|
|
246
|
+
}
|
|
247
|
+
if (stats.size > MAX_MIGRATION_INPUT_BYTES) {
|
|
248
|
+
throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
249
|
+
}
|
|
250
|
+
const bytes = await readFile(resolved);
|
|
251
|
+
if (bytes.length > MAX_MIGRATION_INPUT_BYTES) {
|
|
252
|
+
throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
253
|
+
}
|
|
254
|
+
let text;
|
|
255
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch {
|
|
256
|
+
throw new TodoStoreError('INVALID_JSON', 'invalid_utf8');
|
|
257
|
+
}
|
|
258
|
+
if (text.startsWith('\uFEFF') || text.includes('\r')) {
|
|
259
|
+
throw new TodoStoreError('INVALID_JSON', 'non_portable_json_bytes');
|
|
260
|
+
}
|
|
261
|
+
const parseErrors = [];
|
|
262
|
+
const tree = parseTree(text, parseErrors, { allowTrailingComma: false, disallowComments: true });
|
|
263
|
+
if (parseErrors.length > 0 || tree === undefined) {
|
|
264
|
+
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
265
|
+
}
|
|
266
|
+
if (hasDuplicateJsonKey(tree)) throw new TodoStoreError('INVALID_JSON', 'duplicate_key');
|
|
267
|
+
let descriptor;
|
|
268
|
+
try { descriptor = JSON.parse(text); } catch {
|
|
269
|
+
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
270
|
+
}
|
|
271
|
+
if (!validateEvidenceDescriptor(descriptor)) {
|
|
272
|
+
throw new TodoStoreError('INVALID_EVIDENCE', 'schema_invalid');
|
|
273
|
+
}
|
|
274
|
+
return descriptor;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function mutationActor(env) {
|
|
278
|
+
const entries = ACTOR_ENV_KEYS.map((key) => ({ key, value: env[key] }));
|
|
279
|
+
const missingEnvironment = entries
|
|
280
|
+
.filter(({ value }) => typeof value !== 'string' || value.length === 0)
|
|
281
|
+
.map(({ key }) => key);
|
|
282
|
+
const invalidEnvironment = entries
|
|
283
|
+
.filter(({ value }) => typeof value === 'string' && value.length > 0 && !isTodoIdentifier(value))
|
|
284
|
+
.map(({ key }) => key);
|
|
285
|
+
if (missingEnvironment.length > 0 || invalidEnvironment.length > 0) {
|
|
286
|
+
throw new TodoStoreError('ACTOR_UNRESOLVED', 'actor_environment_invalid', undefined, {
|
|
287
|
+
required_environment: ACTOR_ENV_KEYS,
|
|
288
|
+
missing_environment: missingEnvironment,
|
|
289
|
+
invalid_environment: invalidEnvironment,
|
|
290
|
+
next_action: 'set_required_actor_environment_and_retry',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return { host: entries[0].value, session: entries[1].value, agent: entries[2].value };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function mutate({ repoRoot, env, planKey, taskId, kind, payload, evidenceRef }) {
|
|
297
|
+
const actor = mutationActor(env);
|
|
298
|
+
const evidence = evidenceRef === null ? null : await readEvidenceInput(repoRoot, evidenceRef);
|
|
299
|
+
let eventPayload = payload;
|
|
300
|
+
if (kind === 'done' && payload === 'authored') eventPayload = { evidence };
|
|
301
|
+
if (kind === 'done' && payload === 'evidence_promotion') {
|
|
302
|
+
eventPayload = { done_mode: 'evidence_promotion', imported: true, evidence };
|
|
303
|
+
}
|
|
304
|
+
const { event, snapshot } = await appendTodoEvent({
|
|
305
|
+
repoRoot,
|
|
306
|
+
writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
307
|
+
planKey,
|
|
308
|
+
event: { kind, task_id: taskId, actor, payload: eventPayload },
|
|
309
|
+
});
|
|
310
|
+
const task = snapshot.tasks.find(({ task_id: current }) => current === taskId);
|
|
311
|
+
const result = {
|
|
312
|
+
schema: 'lattice.todo_mutation_result.v1',
|
|
313
|
+
project_id: event.project_id,
|
|
314
|
+
plan_key: event.plan_key,
|
|
315
|
+
plan_version: event.plan_version,
|
|
316
|
+
task_id: event.task_id,
|
|
317
|
+
kind: event.kind,
|
|
318
|
+
sequence: event.sequence,
|
|
319
|
+
event_digest: event.event_digest,
|
|
320
|
+
journal_head_digest: event.event_digest,
|
|
321
|
+
snapshot_digest: snapshot.snapshot_digest,
|
|
322
|
+
status: task.status,
|
|
323
|
+
result_digest: '',
|
|
324
|
+
};
|
|
325
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function startTask({ repoRoot, env, planKey, taskId, overrideReason, parallelFrontier }) {
|
|
330
|
+
const projection = projectTodoStatus(await readTodoStore({ repoRoot }));
|
|
331
|
+
const targetReady = projection.next_ready.some((task) => (
|
|
332
|
+
task.plan_key === planKey && task.task_id === taskId
|
|
333
|
+
));
|
|
334
|
+
if (parallelFrontier && !targetReady) {
|
|
335
|
+
throw new TodoStoreError('PARALLEL_DISPATCH_INVALID', 'parallel_frontier_not_applicable');
|
|
336
|
+
}
|
|
337
|
+
if (targetReady && projection.active_set.length === 0 && projection.next_ready.length > 1
|
|
338
|
+
&& overrideReason === null && !parallelFrontier) {
|
|
339
|
+
throw new TodoStoreError('PARALLEL_DISPATCH_REQUIRED', 'parallel_frontier_requires_declaration',
|
|
340
|
+
undefined, {
|
|
341
|
+
ready_count: projection.next_ready.length,
|
|
342
|
+
frontier_digest: projection.dispatch_frontier.frontier_digest,
|
|
343
|
+
parallel_start_flag: projection.dispatch_frontier.parallel_start_flag,
|
|
344
|
+
serial_reason_flag: '--override-reason',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return mutate({ repoRoot, env, planKey, taskId, kind: 'start',
|
|
348
|
+
payload: { override_reason: overrideReason }, evidenceRef: null });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function validatePhaseDecisionInput(value, outcome) {
|
|
352
|
+
const keys = outcome === 'accept'
|
|
353
|
+
? ['schema', 'review_event_digest', 'decision_evidence', 'evidence_slots', 'input_digest']
|
|
354
|
+
: ['schema', 'review_event_digest', 'reason', 'decision_evidence', 'input_digest'];
|
|
355
|
+
return exactRecord(value, keys) && value.schema === `lattice.phase_${outcome}_input.v1`
|
|
356
|
+
&& isTodoDigest(value.review_event_digest) && validateEvidenceDescriptor(value.decision_evidence)
|
|
357
|
+
&& (outcome === 'accept'
|
|
358
|
+
? Array.isArray(value.evidence_slots) && value.evidence_slots.length > 0
|
|
359
|
+
&& value.evidence_slots.every((entry) => exactRecord(entry, ['slot_id', 'evidence'])
|
|
360
|
+
&& isTodoIdentifier(entry.slot_id) && validateEvidenceDescriptor(entry.evidence))
|
|
361
|
+
&& value.evidence_slots.every((entry, index) => index === 0
|
|
362
|
+
|| value.evidence_slots[index - 1].slot_id < entry.slot_id)
|
|
363
|
+
: typeof value.reason === 'string' && value.reason.length > 0)
|
|
364
|
+
&& isTodoDigest(value.input_digest) && value.input_digest === todoSelfDigest(value, 'input_digest');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function phaseDecision({ repoRoot, env, planKey, phaseId, outcome, inputRef }) {
|
|
368
|
+
const input = await readRevisionInput(repoRoot, inputRef, {
|
|
369
|
+
validate: (value) => validatePhaseDecisionInput(value, outcome),
|
|
370
|
+
invalidCode: 'PHASE_DECISION_INVALID', invalidReason: 'phase_decision_schema_or_digest_invalid',
|
|
371
|
+
});
|
|
372
|
+
const payload = outcome === 'accept'
|
|
373
|
+
? { review_event_digest: input.review_event_digest, decision_evidence: input.decision_evidence,
|
|
374
|
+
evidence_slots: input.evidence_slots }
|
|
375
|
+
: { review_event_digest: input.review_event_digest, reason: input.reason,
|
|
376
|
+
decision_evidence: input.decision_evidence };
|
|
377
|
+
return phaseMutation({ repoRoot, env, planKey, phaseId, kind: `phase_${outcome}`, payload });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function phaseMutation({ repoRoot, env, planKey, phaseId, kind, payload }) {
|
|
381
|
+
const { event, snapshot } = await appendTodoEvent({
|
|
382
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey,
|
|
383
|
+
event: { kind, phase_id: phaseId, actor: mutationActor(env), payload },
|
|
384
|
+
});
|
|
385
|
+
const phase = snapshot.phases?.find(({ phase_id: current }) => current === phaseId);
|
|
386
|
+
if (phase === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'phase_not_active');
|
|
387
|
+
const result = {
|
|
388
|
+
schema: 'lattice.phase_mutation_result.v1', project_id: event.project_id,
|
|
389
|
+
plan_key: event.plan_key, plan_version: event.plan_version, phase_id: event.phase_id,
|
|
390
|
+
kind: event.kind, sequence: event.sequence, event_digest: event.event_digest,
|
|
391
|
+
journal_head_digest: event.event_digest, snapshot_digest: snapshot.snapshot_digest,
|
|
392
|
+
status: phase.status, result_digest: '',
|
|
393
|
+
};
|
|
394
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function phaseStatus({ repoRoot, planKey }) {
|
|
399
|
+
const store = await readTodoStore({ repoRoot });
|
|
400
|
+
const [member] = selectMembers(store, planKey);
|
|
401
|
+
if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(member.plan.schema)) {
|
|
402
|
+
throw new TodoStoreError('PHASE_UNAVAILABLE', 'plan_has_no_phase_contract');
|
|
403
|
+
}
|
|
404
|
+
const result = {
|
|
405
|
+
schema: 'lattice.phase_status_result.v1', project_id: store.project_id,
|
|
406
|
+
plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
|
|
407
|
+
journal_head_digest: member.journal.events.at(-1).event_digest,
|
|
408
|
+
phases: member.snapshot.phases, result_digest: '',
|
|
409
|
+
};
|
|
410
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function migrate({ repoRoot, inputRef }) {
|
|
415
|
+
const extraction = await readMigrationInput(repoRoot, inputRef);
|
|
416
|
+
const imported = await appendTodoExtraction({ repoRoot, extraction });
|
|
417
|
+
const registered = extraction.tasks.filter(({ disposition }) => disposition.startsWith('register_'));
|
|
418
|
+
const result = {
|
|
419
|
+
schema: 'lattice.todo_migrate_result.v1',
|
|
420
|
+
project_id: imported.plan.project_id,
|
|
421
|
+
plan_key: imported.plan.plan_key,
|
|
422
|
+
plan_version: imported.plan.plan_version,
|
|
423
|
+
extraction_digest: extraction.extraction_digest,
|
|
424
|
+
imported_task_count: registered.length,
|
|
425
|
+
completed_task_count: extraction.tasks.filter(({ disposition }) => disposition === 'register_done').length,
|
|
426
|
+
plan_ref: imported.descriptor.plan_ref,
|
|
427
|
+
journal_ref: imported.descriptor.journal_ref,
|
|
428
|
+
snapshot_ref: imported.descriptor.snapshot_ref,
|
|
429
|
+
topology_digest: imported.plan.topology_digest,
|
|
430
|
+
journal_head_digest: imported.events.at(-1).event_digest,
|
|
431
|
+
result_digest: '',
|
|
432
|
+
};
|
|
433
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function revise({ repoRoot, env, planKey, inputRef }) {
|
|
438
|
+
const revision = await readRevisionInput(repoRoot, inputRef);
|
|
439
|
+
if (revision.plan_key !== planKey) {
|
|
440
|
+
throw new TodoStoreError('REVISION_INVALID', 'requested_plan_mismatch');
|
|
441
|
+
}
|
|
442
|
+
return applyTodoRevision({
|
|
443
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), revision,
|
|
444
|
+
actor: mutationActor(env), recordedAt: new Date().toISOString(),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function reviseSet({ repoRoot, env, inputRef }) {
|
|
449
|
+
const revisionSet = await readRevisionInput(repoRoot, inputRef, {
|
|
450
|
+
validate: validateTodoRevisionSet,
|
|
451
|
+
invalidCode: 'REVISION_SET_INVALID',
|
|
452
|
+
invalidReason: 'revision_set_schema_invalid',
|
|
453
|
+
});
|
|
454
|
+
return applyTodoRevisionSet({
|
|
455
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), revisionSet,
|
|
456
|
+
actor: mutationActor(env), recordedAt: new Date().toISOString(),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function revisePhase({ repoRoot, env, planKey, inputRef }) {
|
|
461
|
+
const revision = await readRevisionInput(repoRoot, inputRef, {
|
|
462
|
+
validate: validatePhaseTodoRevision, invalidCode: 'REVISION_INVALID',
|
|
463
|
+
invalidReason: 'phase_revision_schema_or_digest_invalid',
|
|
464
|
+
});
|
|
465
|
+
if (revision.plan_key !== planKey) throw new TodoStoreError('REVISION_INVALID', 'requested_plan_mismatch');
|
|
466
|
+
return applyPhaseTodoRevision({ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
467
|
+
revision, actor: mutationActor(env), recordedAt: new Date().toISOString() });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function status({ repoRoot }) {
|
|
471
|
+
return projectTodoStatus(await readTodoStore({ repoRoot }));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function readNarrative(repoRoot, ref) {
|
|
475
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
476
|
+
const source = parseTodoSourceRef(ref);
|
|
477
|
+
const fileRef = source?.path ?? ref;
|
|
478
|
+
const absolute = path.resolve(canonicalRoot, fileRef);
|
|
479
|
+
if (!within(canonicalRoot, absolute)) {
|
|
480
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'narrative_path_outside_repo', undefined, { ref });
|
|
481
|
+
}
|
|
482
|
+
let stats;
|
|
483
|
+
try { stats = await lstat(absolute); } catch {
|
|
484
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'narrative_missing', undefined, { ref });
|
|
485
|
+
}
|
|
486
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
487
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'unsafe_narrative_path', undefined, { ref });
|
|
488
|
+
}
|
|
489
|
+
const resolved = await realpath(absolute);
|
|
490
|
+
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
491
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'narrative_path_alias_or_escape', undefined, { ref });
|
|
492
|
+
}
|
|
493
|
+
const fileBytes = await readFile(resolved);
|
|
494
|
+
let bytes = fileBytes;
|
|
495
|
+
if (source !== null) {
|
|
496
|
+
const lines = [];
|
|
497
|
+
let start = 0;
|
|
498
|
+
for (let index = 0; index <= fileBytes.length; index += 1) {
|
|
499
|
+
if (index === fileBytes.length || fileBytes[index] === 0x0a) {
|
|
500
|
+
lines.push(fileBytes.subarray(start, index));
|
|
501
|
+
start = index + 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
bytes = lines[source.line - 1];
|
|
505
|
+
if (bytes === undefined) {
|
|
506
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'narrative_line_missing', undefined, { ref });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
let markdown;
|
|
510
|
+
try { markdown = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch {
|
|
511
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'narrative_invalid_utf8', undefined, { ref });
|
|
512
|
+
}
|
|
513
|
+
return { markdown, content_digest: createHash('sha256').update(bytes).digest('hex') };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function loadNarratives(store, repoRoot) {
|
|
517
|
+
const content = new Map();
|
|
518
|
+
const bindings = [];
|
|
519
|
+
const narratives = [];
|
|
520
|
+
for (const member of store.members) for (const task of member.plan.tasks) {
|
|
521
|
+
const ref = taskRef(member.plan, task.task_id);
|
|
522
|
+
if (task.narrative_ref === null) {
|
|
523
|
+
bindings.push({ ...ref, narrative_ref: null, content_digest: null });
|
|
524
|
+
narratives.push({ ref, markdown: '', narrative_ref: null, content_digest: null });
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
let loaded = content.get(task.narrative_ref);
|
|
528
|
+
if (loaded === undefined) {
|
|
529
|
+
loaded = await readNarrative(repoRoot, task.narrative_ref);
|
|
530
|
+
content.set(task.narrative_ref, loaded);
|
|
531
|
+
}
|
|
532
|
+
bindings.push({ ...ref, narrative_ref: task.narrative_ref, content_digest: loaded.content_digest });
|
|
533
|
+
narratives.push({ ref, markdown: loaded.markdown, narrative_ref: task.narrative_ref,
|
|
534
|
+
content_digest: loaded.content_digest });
|
|
535
|
+
}
|
|
536
|
+
return { narratives, bindings };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function resolveOutput(repoRoot, outputRef) {
|
|
540
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
541
|
+
const absolute = path.resolve(canonicalRoot, outputRef);
|
|
542
|
+
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
543
|
+
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'output_path_outside_repo', undefined, { output_ref: outputRef });
|
|
544
|
+
}
|
|
545
|
+
let cursor = path.dirname(absolute);
|
|
546
|
+
const missing = [];
|
|
547
|
+
while (cursor !== canonicalRoot) {
|
|
548
|
+
try {
|
|
549
|
+
const stats = await lstat(cursor);
|
|
550
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
551
|
+
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'unsafe_output_parent', undefined, { output_ref: outputRef });
|
|
552
|
+
}
|
|
553
|
+
const resolved = await realpath(cursor);
|
|
554
|
+
if (resolved !== cursor || !within(canonicalRoot, resolved)) {
|
|
555
|
+
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'output_parent_alias_or_escape', undefined, { output_ref: outputRef });
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof TodoStoreError) throw error;
|
|
560
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
561
|
+
missing.push(cursor);
|
|
562
|
+
cursor = path.dirname(cursor);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
let target;
|
|
566
|
+
try { target = await lstat(absolute); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
567
|
+
if (target !== undefined && (target.isSymbolicLink() || !target.isFile())) {
|
|
568
|
+
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'unsafe_output_target', undefined, { output_ref: outputRef });
|
|
569
|
+
}
|
|
570
|
+
return { absolute, missing, target };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function atomicWriteOutput(repoRoot, outputRef, html) {
|
|
574
|
+
const { absolute } = await resolveOutput(repoRoot, outputRef);
|
|
575
|
+
await mkdir(path.dirname(absolute), { recursive: true });
|
|
576
|
+
// Re-resolve after mkdir so a raced alias cannot redirect the write.
|
|
577
|
+
const checked = await resolveOutput(repoRoot, outputRef);
|
|
578
|
+
const temporary = path.join(path.dirname(checked.absolute),
|
|
579
|
+
`.${path.basename(checked.absolute)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
|
|
580
|
+
let handle;
|
|
581
|
+
try {
|
|
582
|
+
handle = await open(temporary, 'wx', 0o600);
|
|
583
|
+
await handle.writeFile(html, 'utf8');
|
|
584
|
+
await handle.sync();
|
|
585
|
+
await handle.close();
|
|
586
|
+
handle = null;
|
|
587
|
+
await rename(temporary, checked.absolute);
|
|
588
|
+
} finally {
|
|
589
|
+
if (handle) await handle.close();
|
|
590
|
+
await rm(temporary, { force: true });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function ganttDescriptorRef(outputRef) {
|
|
595
|
+
return `${outputRef}${GANTT_DESCRIPTOR_SUFFIX}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function validateGanttArtifactDescriptor(value) {
|
|
599
|
+
return exactRecord(value, [
|
|
600
|
+
'schema', 'project_id', 'output_ref', 'manifest_digest', 'renderer_version',
|
|
601
|
+
'html_digest', 'artifact_digest',
|
|
602
|
+
]) && value.schema === 'lattice.todo_gantt_artifact.v1'
|
|
603
|
+
&& isTodoIdentifier(value.project_id) && isTodoRef(value.output_ref)
|
|
604
|
+
&& isTodoDigest(value.manifest_digest)
|
|
605
|
+
&& typeof value.renderer_version === 'string'
|
|
606
|
+
&& /^lattice\.todo_gantt_renderer\.v[1-9][0-9]*$/u.test(value.renderer_version)
|
|
607
|
+
&& isTodoDigest(value.html_digest) && isTodoDigest(value.artifact_digest)
|
|
608
|
+
&& value.artifact_digest === todoSelfDigest(value, 'artifact_digest');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function readOptionalOutput(repoRoot, outputRef, maxBytes) {
|
|
612
|
+
const resolved = await resolveOutput(repoRoot, outputRef);
|
|
613
|
+
if (resolved.target === undefined) return null;
|
|
614
|
+
if (resolved.target.size > maxBytes) {
|
|
615
|
+
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_size_limit_exceeded', undefined,
|
|
616
|
+
{ output_ref: outputRef });
|
|
617
|
+
}
|
|
618
|
+
return readFile(resolved.absolute);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function parseGanttDescriptor(bytes, descriptorRef) {
|
|
622
|
+
let value;
|
|
623
|
+
try {
|
|
624
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
625
|
+
value = JSON.parse(text);
|
|
626
|
+
if (`${canonicalizeTodoArtifact(value)}\n` !== text || !validateGanttArtifactDescriptor(value)) {
|
|
627
|
+
throw new Error('non-canonical descriptor');
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'descriptor_invalid', undefined,
|
|
631
|
+
{ descriptor_ref: descriptorRef });
|
|
632
|
+
}
|
|
633
|
+
return value;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export async function renderTodoGanttForProject({
|
|
637
|
+
repoRoot, stable = false, displayName = null, env = process.env,
|
|
638
|
+
}) {
|
|
639
|
+
const store = stable ? await readTodoStoreStable({ repoRoot }) : await readTodoStore({ repoRoot });
|
|
640
|
+
const identity = displayName === null
|
|
641
|
+
? await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env })
|
|
642
|
+
: { displayName };
|
|
643
|
+
const presentation = await loadTodoGanttPresentation({ repoRoot, readModel: store });
|
|
644
|
+
const topology = mergedTopology(store);
|
|
645
|
+
const chain = projectTodoChainV1(topology);
|
|
646
|
+
const layout = layoutTodoGantt(store, chain);
|
|
647
|
+
const narrative = await loadNarratives(store, repoRoot);
|
|
648
|
+
const anchorOutcomes = verifyNarrativeAnchors({
|
|
649
|
+
readModel: store,
|
|
650
|
+
narratives: narrative.narratives,
|
|
651
|
+
});
|
|
652
|
+
const outcomesByRef = new Map(anchorOutcomes.map((entry) => [
|
|
653
|
+
JSON.stringify([entry.ref.project_id, entry.ref.plan_key, entry.ref.task_id]), entry,
|
|
654
|
+
]));
|
|
655
|
+
const narrativeBindings = narrative.bindings.map((entry) => {
|
|
656
|
+
const key = JSON.stringify([entry.project_id, entry.plan_key, entry.task_id]);
|
|
657
|
+
const anchor = outcomesByRef.get(key);
|
|
658
|
+
return { ...entry, anchored: anchor.anchored, reason: anchor.reason };
|
|
659
|
+
});
|
|
660
|
+
const memberBindings = store.members.map((member) => ({
|
|
661
|
+
plan_key: member.descriptor.plan_key,
|
|
662
|
+
topology_digest: member.plan.topology_digest,
|
|
663
|
+
journal_head_digest: member.journal.events.at(-1).event_digest,
|
|
664
|
+
}));
|
|
665
|
+
const metadata = {
|
|
666
|
+
manifest_digest: store.manifest.manifest_digest,
|
|
667
|
+
member_bindings: memberBindings,
|
|
668
|
+
narrative_bindings_digest: digestTodoArtifact(narrativeBindings),
|
|
669
|
+
presentation_digest: presentation.presentation_digest,
|
|
670
|
+
chain_digest: digestTodoArtifact(chain),
|
|
671
|
+
layout_digest: digestTodoArtifact(layout),
|
|
672
|
+
renderer_version: TODO_GANTT_RENDERER_VERSION,
|
|
673
|
+
project_display_name: identity.displayName,
|
|
674
|
+
};
|
|
675
|
+
const rendered = renderTodoGanttHtml({
|
|
676
|
+
readModel: store,
|
|
677
|
+
layout,
|
|
678
|
+
narratives: narrative.narratives,
|
|
679
|
+
anchorOutcomes,
|
|
680
|
+
presentation,
|
|
681
|
+
metadata,
|
|
682
|
+
});
|
|
683
|
+
return { store, metadata, memberBindings, rendered };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function gantt({ repoRoot, outputRef, env }) {
|
|
687
|
+
const { store, metadata, memberBindings, rendered } = await renderTodoGanttForProject({ repoRoot, env });
|
|
688
|
+
await atomicWriteOutput(repoRoot, outputRef, rendered.html);
|
|
689
|
+
const descriptor = { schema: 'lattice.todo_gantt_artifact.v1', project_id: store.project_id,
|
|
690
|
+
output_ref: outputRef, manifest_digest: metadata.manifest_digest,
|
|
691
|
+
renderer_version: TODO_GANTT_RENDERER_VERSION, html_digest: rendered.html_digest,
|
|
692
|
+
artifact_digest: '' };
|
|
693
|
+
descriptor.artifact_digest = todoSelfDigest(descriptor, 'artifact_digest');
|
|
694
|
+
await atomicWriteOutput(repoRoot, ganttDescriptorRef(outputRef),
|
|
695
|
+
`${canonicalizeTodoArtifact(descriptor)}\n`);
|
|
696
|
+
const result = {
|
|
697
|
+
schema: 'lattice.todo_gantt_result.v1',
|
|
698
|
+
project_id: store.project_id,
|
|
699
|
+
output_ref: outputRef,
|
|
700
|
+
manifest_digest: metadata.manifest_digest,
|
|
701
|
+
member_bindings: memberBindings,
|
|
702
|
+
narrative_bindings_digest: metadata.narrative_bindings_digest,
|
|
703
|
+
chain_digest: metadata.chain_digest,
|
|
704
|
+
layout_digest: metadata.layout_digest,
|
|
705
|
+
renderer_version: TODO_GANTT_RENDERER_VERSION,
|
|
706
|
+
html_digest: rendered.html_digest,
|
|
707
|
+
result_digest: '',
|
|
708
|
+
};
|
|
709
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function ganttStatus({ repoRoot, outputRef, env }) {
|
|
714
|
+
const descriptorRef = ganttDescriptorRef(outputRef);
|
|
715
|
+
const [htmlBytes, descriptorBytes] = await Promise.all([
|
|
716
|
+
readOptionalOutput(repoRoot, outputRef, TODO_GANTT_HTML_MAX_BYTES),
|
|
717
|
+
readOptionalOutput(repoRoot, descriptorRef, MAX_GANTT_DESCRIPTOR_BYTES),
|
|
718
|
+
]);
|
|
719
|
+
if ((htmlBytes === null) !== (descriptorBytes === null)) {
|
|
720
|
+
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_pair_incomplete', undefined,
|
|
721
|
+
{ output_ref: outputRef, descriptor_ref: descriptorRef });
|
|
722
|
+
}
|
|
723
|
+
if (htmlBytes === null) {
|
|
724
|
+
const store = await readTodoStore({ repoRoot });
|
|
725
|
+
const result = { schema: 'lattice.todo_gantt_status_result.v1', project_id: store.project_id,
|
|
726
|
+
output_ref: outputRef, descriptor_ref: descriptorRef, artifact_status: 'missing',
|
|
727
|
+
current_manifest_digest: store.manifest.manifest_digest, artifact_manifest_digest: null,
|
|
728
|
+
html_digest: null, renderer_version: null, result_digest: '' };
|
|
729
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
const descriptor = parseGanttDescriptor(descriptorBytes, descriptorRef);
|
|
733
|
+
const htmlDigest = createHash('sha256').update(htmlBytes).digest('hex');
|
|
734
|
+
if (descriptor.output_ref !== outputRef || descriptor.html_digest !== htmlDigest) {
|
|
735
|
+
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_digest_mismatch', undefined,
|
|
736
|
+
{ output_ref: outputRef, descriptor_ref: descriptorRef });
|
|
737
|
+
}
|
|
738
|
+
const current = await renderTodoGanttForProject({ repoRoot, env });
|
|
739
|
+
if (descriptor.project_id !== current.store.project_id) {
|
|
740
|
+
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_project_mismatch', undefined,
|
|
741
|
+
{ output_ref: outputRef });
|
|
742
|
+
}
|
|
743
|
+
const artifactStatus = descriptor.renderer_version === TODO_GANTT_RENDERER_VERSION
|
|
744
|
+
&& descriptor.manifest_digest === current.metadata.manifest_digest
|
|
745
|
+
&& descriptor.html_digest === current.rendered.html_digest ? 'current' : 'stale';
|
|
746
|
+
const result = { schema: 'lattice.todo_gantt_status_result.v1',
|
|
747
|
+
project_id: current.store.project_id, output_ref: outputRef, descriptor_ref: descriptorRef,
|
|
748
|
+
artifact_status: artifactStatus, current_manifest_digest: current.metadata.manifest_digest,
|
|
749
|
+
artifact_manifest_digest: descriptor.manifest_digest, html_digest: descriptor.html_digest,
|
|
750
|
+
renderer_version: descriptor.renderer_version, result_digest: '' };
|
|
751
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function serveGantt({ repoRoot, port, stdout, env }) {
|
|
756
|
+
const initialStore = await readTodoStoreStable({ repoRoot });
|
|
757
|
+
const identity = await resolveProjectIdentity({ repoRoot, projectId: initialStore.project_id, env });
|
|
758
|
+
const live = await startTodoGanttLiveServer({
|
|
759
|
+
projectId: initialStore.project_id,
|
|
760
|
+
displayName: identity.displayName,
|
|
761
|
+
port,
|
|
762
|
+
render: async () => {
|
|
763
|
+
const { rendered, metadata } = await renderTodoGanttForProject({
|
|
764
|
+
repoRoot, stable: true, displayName: identity.displayName,
|
|
765
|
+
});
|
|
766
|
+
return { html: rendered.html, head_digest: metadata.manifest_digest };
|
|
767
|
+
},
|
|
768
|
+
readHead: async () => (await readTodoStoreStable({ repoRoot })).manifest.manifest_digest,
|
|
769
|
+
});
|
|
770
|
+
const result = { schema: 'lattice.todo_gantt_live_result.v2', project_id: live.projectId,
|
|
771
|
+
host: live.host, port: live.port, project_path: live.projectPath,
|
|
772
|
+
url: live.url, events_url: live.eventsUrl };
|
|
773
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
774
|
+
await new Promise((resolve) => {
|
|
775
|
+
const stop = () => resolve();
|
|
776
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
777
|
+
});
|
|
778
|
+
await live.close();
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function ensureActiveProjectDashboard({ repoRoot, env }) {
|
|
783
|
+
if (env.LATTICE_DASHBOARD_AUTOSTART === '0') return null;
|
|
784
|
+
const actorIdentity = ACTOR_ENV_KEYS.map((key) => env[key]);
|
|
785
|
+
if (!actorIdentity.every(isTodoIdentifier)) return null;
|
|
786
|
+
const sessionId = env.LATTICE_TODO_ACTOR_SESSION;
|
|
787
|
+
const store = await readTodoStoreStable({ repoRoot });
|
|
788
|
+
let identity;
|
|
789
|
+
try { identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env }); } catch (error) {
|
|
790
|
+
throw new TodoStoreError(error?.code ?? 'PROJECT_IDENTITY_INVALID',
|
|
791
|
+
'project_identity_resolve_failed', undefined, error?.detail ?? {});
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
return await ensureTodoDashboardActivity({
|
|
795
|
+
repoRoot, projectId: store.project_id, displayName: identity.displayName, sessionId, env,
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
throw new TodoStoreError(error?.code ?? 'DASHBOARD_DAEMON_UNAVAILABLE',
|
|
799
|
+
'dashboard_daemon_ensure_failed', undefined, { project_id: store.project_id });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function verify({ repoRoot, requestedPlanKey }) {
|
|
804
|
+
const store = await readTodoStore({ repoRoot });
|
|
805
|
+
const members = selectMembers(store, requestedPlanKey);
|
|
806
|
+
for (const member of members) {
|
|
807
|
+
const unverified = member.tasks.find((task) => task.evidence_unverified);
|
|
808
|
+
if (unverified !== undefined) {
|
|
809
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'evidence_unverified', 'evidence_unverified', {
|
|
810
|
+
plan_key: member.descriptor.plan_key,
|
|
811
|
+
task_id: unverified.task_id,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (member.revision !== null) {
|
|
815
|
+
switch (member.revision.schema) {
|
|
816
|
+
case 'lattice.todo_revision.v1':
|
|
817
|
+
case 'lattice.todo_revision.v2':
|
|
818
|
+
await verifyTodoRevisionSources({ repoRoot, revision: member.revision });
|
|
819
|
+
break;
|
|
820
|
+
case 'lattice.phase_todo_revision.v1':
|
|
821
|
+
case 'lattice.phase_todo_revision.v2':
|
|
822
|
+
break;
|
|
823
|
+
case 'lattice.phase_todo_revision.v3':
|
|
824
|
+
await verifyPhaseTodoRevisionSources({ repoRoot, revision: member.revision });
|
|
825
|
+
break;
|
|
826
|
+
default:
|
|
827
|
+
throw new TodoStoreError('REVISION_INVALID', 'revision_schema_or_digest_invalid');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const verifiedMembers = members.map((member) => {
|
|
832
|
+
const reconciled = member.revision !== null;
|
|
833
|
+
const phaseRevision = ['lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2',
|
|
834
|
+
'lattice.phase_todo_revision.v3']
|
|
835
|
+
.includes(member.revision?.schema);
|
|
836
|
+
return {
|
|
837
|
+
plan_key: member.descriptor.plan_key,
|
|
838
|
+
plan_version: member.plan.plan_version,
|
|
839
|
+
topology_digest: member.plan.topology_digest,
|
|
840
|
+
journal_head_digest: member.journal.events.at(-1).event_digest,
|
|
841
|
+
through_sequence: member.journal.events.at(-1).sequence,
|
|
842
|
+
snapshot_stale: member.snapshot_stale,
|
|
843
|
+
reconciliation_state: reconciled ? 'reconciled' : 'registered_unreconciled',
|
|
844
|
+
revision_digest: reconciled ? member.revision.revision_digest : null,
|
|
845
|
+
reconciliation_digest: reconciled
|
|
846
|
+
? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3'
|
|
847
|
+
? member.revision.revision_digest : member.revision.reconciliation.reconciliation_digest
|
|
848
|
+
: todoLegacyReconciliationDigest({ planDigest: member.plan.plan_digest,
|
|
849
|
+
journalHeadDigest: member.journal.events.at(-1).event_digest }),
|
|
850
|
+
source_inventory_count: reconciled
|
|
851
|
+
? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3' ? 0
|
|
852
|
+
: member.revision.source_inventory.active.length
|
|
853
|
+
+ member.revision.source_inventory.excluded_tombstones.length : null,
|
|
854
|
+
active_task_count: reconciled
|
|
855
|
+
? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3'
|
|
856
|
+
? 0 : member.revision.source_inventory.active.length : null,
|
|
857
|
+
excluded_tombstone_count: reconciled
|
|
858
|
+
? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3'
|
|
859
|
+
? 0 : member.revision.source_inventory.excluded_tombstones.length : null,
|
|
860
|
+
};
|
|
861
|
+
});
|
|
862
|
+
const result = {
|
|
863
|
+
schema: 'lattice.todo_verify_result.v2',
|
|
864
|
+
project_id: store.project_id,
|
|
865
|
+
requested_plan_key: requestedPlanKey,
|
|
866
|
+
verified_members: verifiedMembers,
|
|
867
|
+
snapshot_stale: verifiedMembers.some((member) => member.snapshot_stale),
|
|
868
|
+
result_digest: '',
|
|
869
|
+
};
|
|
870
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function rebuildSnapshot({ repoRoot, planKey }) {
|
|
875
|
+
// Read first so every typed validation failure happens before the rebuild writer is entered.
|
|
876
|
+
const store = await readTodoStore({ repoRoot });
|
|
877
|
+
const [member] = selectMembers(store, planKey);
|
|
878
|
+
const snapshot = await rebuildTodoSnapshot({ repoRoot, planKey });
|
|
879
|
+
const result = {
|
|
880
|
+
schema: 'lattice.todo_snapshot_result.v1',
|
|
881
|
+
project_id: store.project_id,
|
|
882
|
+
plan_key: planKey,
|
|
883
|
+
snapshot_ref: member.descriptor.snapshot_ref,
|
|
884
|
+
through_sequence: snapshot.through_sequence,
|
|
885
|
+
journal_head_digest: snapshot.journal_head_digest,
|
|
886
|
+
snapshot_digest: snapshot.snapshot_digest,
|
|
887
|
+
result_digest: '',
|
|
888
|
+
};
|
|
889
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
890
|
+
return result;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* `lattice todo` namespace. Exact position, order, and argument count are part of
|
|
895
|
+
* the public contract; usage failures never use a JSON envelope.
|
|
896
|
+
*/
|
|
897
|
+
export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env }) {
|
|
898
|
+
if (!Array.isArray(argv) || typeof cwd !== 'string'
|
|
899
|
+
|| typeof stdout?.write !== 'function' || typeof stderr?.write !== 'function'
|
|
900
|
+
|| env === null || typeof env !== 'object' || Array.isArray(env)) {
|
|
901
|
+
throw new TypeError('runTodoCli optionsが不正');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
let action = null;
|
|
905
|
+
if ((argv.length === 1 && argv[0] === 'status')
|
|
906
|
+
|| (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
|
|
907
|
+
action = (repoRoot) => status({ repoRoot });
|
|
908
|
+
} else if ((argv.length === 1 && argv[0] === 'verify')
|
|
909
|
+
|| (argv.length === 2 && argv[0] === 'verify' && argv[1] === '--json')) {
|
|
910
|
+
action = (repoRoot) => verify({ repoRoot, requestedPlanKey: null });
|
|
911
|
+
} else if ((argv.length === 3 || argv.length === 4) && argv[0] === 'verify'
|
|
912
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
913
|
+
&& (argv.length === 3 || argv[3] === '--json')) {
|
|
914
|
+
action = (repoRoot) => verify({ repoRoot, requestedPlanKey: argv[2] });
|
|
915
|
+
} else if (argv.length === 4 && argv[0] === 'snapshot' && argv[1] === '--rebuild'
|
|
916
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])) {
|
|
917
|
+
action = (repoRoot) => rebuildSnapshot({ repoRoot, planKey: argv[3] });
|
|
918
|
+
} else if (argv.length === 1 && argv[0] === 'gantt') {
|
|
919
|
+
action = (repoRoot) => gantt({ repoRoot, outputRef: DEFAULT_GANTT_REF, env });
|
|
920
|
+
} else if (argv.length === 3 && argv[0] === 'gantt' && argv[1] === '--out'
|
|
921
|
+
&& isTodoRef(argv[2])) {
|
|
922
|
+
action = (repoRoot) => gantt({ repoRoot, outputRef: argv[2], env });
|
|
923
|
+
} else if (argv.length === 2 && argv[0] === 'gantt' && argv[1] === 'status') {
|
|
924
|
+
action = (repoRoot) => ganttStatus({ repoRoot, outputRef: DEFAULT_GANTT_REF, env });
|
|
925
|
+
} else if (argv.length === 4 && argv[0] === 'gantt' && argv[1] === 'status'
|
|
926
|
+
&& argv[2] === '--out' && isTodoRef(argv[3])) {
|
|
927
|
+
action = (repoRoot) => ganttStatus({ repoRoot, outputRef: argv[3], env });
|
|
928
|
+
} else if (argv.length === 4 && argv[0] === 'gantt' && argv[1] === 'serve'
|
|
929
|
+
&& argv[2] === '--port' && /^(?:0|[1-9][0-9]{0,4})$/u.test(argv[3])
|
|
930
|
+
&& Number(argv[3]) <= 65_535) {
|
|
931
|
+
action = (repoRoot) => serveGantt({ repoRoot, port: Number(argv[3]), stdout, env });
|
|
932
|
+
} else if (argv.length === 3 && argv[0] === 'migrate' && argv[1] === '--input'
|
|
933
|
+
&& isTodoRef(argv[2])) {
|
|
934
|
+
action = (repoRoot) => migrate({ repoRoot, inputRef: argv[2] });
|
|
935
|
+
} else if (argv.length === 5 && argv[0] === 'revise'
|
|
936
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
937
|
+
&& argv[3] === '--input' && isTodoRef(argv[4])) {
|
|
938
|
+
action = (repoRoot) => revise({ repoRoot, env, planKey: argv[2], inputRef: argv[4] });
|
|
939
|
+
} else if (argv.length === 3 && argv[0] === 'revise-set'
|
|
940
|
+
&& argv[1] === '--input' && isTodoRef(argv[2])) {
|
|
941
|
+
action = (repoRoot) => reviseSet({ repoRoot, env, inputRef: argv[2] });
|
|
942
|
+
} else if (argv.length === 5 && argv[0] === 'revise-phase'
|
|
943
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
944
|
+
&& argv[3] === '--input' && isTodoRef(argv[4])) {
|
|
945
|
+
action = (repoRoot) => revisePhase({ repoRoot, env, planKey: argv[2], inputRef: argv[4] });
|
|
946
|
+
} else if (argv.length === 4 && argv[0] === 'phase' && argv[1] === 'status'
|
|
947
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])) {
|
|
948
|
+
action = (repoRoot) => phaseStatus({ repoRoot, planKey: argv[3] });
|
|
949
|
+
} else if (argv.length === 8 && argv[0] === 'phase' && argv[1] === 'review'
|
|
950
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
951
|
+
&& argv[4] === '--phase' && isTodoIdentifier(argv[5])
|
|
952
|
+
&& argv[6] === '--reason' && argv[7].length > 0) {
|
|
953
|
+
action = (repoRoot) => phaseMutation({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
|
|
954
|
+
kind: 'phase_review', payload: { reason: argv[7] } });
|
|
955
|
+
} else if (argv.length === 8 && argv[0] === 'phase'
|
|
956
|
+
&& ['accept', 'reject'].includes(argv[1])
|
|
957
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
958
|
+
&& argv[4] === '--phase' && isTodoIdentifier(argv[5])
|
|
959
|
+
&& argv[6] === '--input' && isTodoRef(argv[7])) {
|
|
960
|
+
action = (repoRoot) => phaseDecision({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
|
|
961
|
+
outcome: argv[1], inputRef: argv[7] });
|
|
962
|
+
} else if ((argv.length === 8 || argv.length === 10) && argv[0] === 'phase'
|
|
963
|
+
&& argv[1] === 'reopen' && argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
964
|
+
&& argv[4] === '--phase' && isTodoIdentifier(argv[5])
|
|
965
|
+
&& argv[6] === '--reason' && argv[7].length > 0
|
|
966
|
+
&& (argv.length === 8 || (argv[8] === '--override-reason' && argv[9].length > 0))) {
|
|
967
|
+
action = (repoRoot) => phaseMutation({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
|
|
968
|
+
kind: 'phase_reopen', payload: { reason: argv[7], override_reason: argv[9] ?? null } });
|
|
969
|
+
} else if ((argv.length === 5 || argv.length === 6 || argv.length === 7) && argv[0] === 'start'
|
|
970
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
971
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
972
|
+
&& (argv.length === 5 || (argv.length === 6 && argv[5] === '--parallel-frontier')
|
|
973
|
+
|| (argv.length === 7 && argv[5] === '--override-reason' && argv[6].length > 0))) {
|
|
974
|
+
const overrideReason = argv.length === 7 ? argv[6] : null;
|
|
975
|
+
action = (repoRoot) => startTask({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
976
|
+
overrideReason, parallelFrontier: argv.length === 6 });
|
|
977
|
+
} else if (argv.length === 7 && argv[0] === 'block'
|
|
978
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
979
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
980
|
+
&& argv[5] === '--reason' && argv[6].length > 0) {
|
|
981
|
+
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
982
|
+
kind: 'block', payload: { reason: argv[6] }, evidenceRef: null });
|
|
983
|
+
} else if (argv.length === 5 && argv[0] === 'unblock'
|
|
984
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
985
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])) {
|
|
986
|
+
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
987
|
+
kind: 'unblock', payload: {}, evidenceRef: null });
|
|
988
|
+
} else if (argv.length === 7 && argv[0] === 'done'
|
|
989
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
990
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
991
|
+
&& argv[5] === '--evidence' && isTodoRef(argv[6])) {
|
|
992
|
+
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
993
|
+
kind: 'done', payload: 'authored', evidenceRef: argv[6] });
|
|
994
|
+
} else if (argv.length === 8 && argv[0] === 'evidence' && argv[1] === 'promote'
|
|
995
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
996
|
+
&& argv[4] === '--task' && isTodoIdentifier(argv[5])
|
|
997
|
+
&& argv[6] === '--evidence' && isTodoRef(argv[7])) {
|
|
998
|
+
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[3], taskId: argv[5],
|
|
999
|
+
kind: 'done', payload: 'evidence_promotion', evidenceRef: argv[7] });
|
|
1000
|
+
} else if ((argv.length === 7 || argv.length === 9) && argv[0] === 'reopen'
|
|
1001
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
1002
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
1003
|
+
&& argv[5] === '--reason' && argv[6].length > 0
|
|
1004
|
+
&& (argv.length === 7 || (argv[7] === '--override-reason' && argv[8].length > 0))) {
|
|
1005
|
+
const overrideReason = argv.length === 9 ? argv[8] : null;
|
|
1006
|
+
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
1007
|
+
kind: 'reopen', payload: { reason: argv[6], override_reason: overrideReason }, evidenceRef: null });
|
|
1008
|
+
}
|
|
1009
|
+
if (action === null) return usageFailure(stderr, argv);
|
|
1010
|
+
|
|
1011
|
+
try {
|
|
1012
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
1013
|
+
const manualServe = argv[0] === 'gantt' && argv[1] === 'serve';
|
|
1014
|
+
if (!manualServe) await ensureActiveProjectDashboard({ repoRoot, env });
|
|
1015
|
+
const result = await action(repoRoot);
|
|
1016
|
+
if (result !== null) stdout.write(`${JSON.stringify(result)}\n`);
|
|
1017
|
+
return 0;
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
if (error instanceof TodoStoreError || (typeof error?.code === 'string'
|
|
1020
|
+
&& error.detail !== null && typeof error.detail === 'object')) return typedFailure(stderr, error);
|
|
1021
|
+
if (error instanceof TypeError) {
|
|
1022
|
+
return typedFailure(stderr, {
|
|
1023
|
+
code: 'CONTRACT_VIOLATION',
|
|
1024
|
+
message: error.message,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return typedFailure(stderr, {
|
|
1028
|
+
code: 'INTERNAL_FAILURE',
|
|
1029
|
+
message: error?.constructor?.name ?? 'Error',
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|