@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/** Hash bytes without imposing an artifact schema or serialization limit. */
|
|
4
|
+
export function sha256Bytes(bytes) {
|
|
5
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Compute a self digest using the caller's canonical serializer. */
|
|
9
|
+
export function digestWithoutField(value, digestField, canonicalize) {
|
|
10
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
11
|
+
throw new TypeError('chain value must be a plain object');
|
|
12
|
+
}
|
|
13
|
+
const projection = {};
|
|
14
|
+
for (const key of Object.keys(value)) {
|
|
15
|
+
if (key !== digestField) projection[key] = value[key];
|
|
16
|
+
}
|
|
17
|
+
return sha256Bytes(Buffer.from(canonicalize(projection), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify the storage order, sequence, genesis, self digest and previous link of
|
|
22
|
+
* a linear hash chain. Domain validators remain with each store family.
|
|
23
|
+
*/
|
|
24
|
+
export function verifyLinearHashChain({
|
|
25
|
+
entries,
|
|
26
|
+
canonicalize,
|
|
27
|
+
digestField,
|
|
28
|
+
sequenceField = 'sequence',
|
|
29
|
+
previousField = 'previous_digest',
|
|
30
|
+
genesisPrevious = null,
|
|
31
|
+
}) {
|
|
32
|
+
const failures = new Set();
|
|
33
|
+
const bySequence = new Map();
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const sequence = entry?.[sequenceField];
|
|
36
|
+
let bytes;
|
|
37
|
+
try {
|
|
38
|
+
bytes = canonicalize(entry);
|
|
39
|
+
if (entry[digestField] !== digestWithoutField(entry, digestField, canonicalize)) {
|
|
40
|
+
failures.add('event_digest_mismatch');
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
failures.add('event_shape');
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const seen = bySequence.get(sequence);
|
|
47
|
+
if (seen === undefined) bySequence.set(sequence, { entries: [entry], bytes: new Set([bytes]) });
|
|
48
|
+
else {
|
|
49
|
+
seen.entries.push(entry);
|
|
50
|
+
seen.bytes.add(bytes);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const seen of bySequence.values()) {
|
|
54
|
+
if (seen.entries.length > 1 && seen.bytes.size === 1) failures.add('duplicate_event');
|
|
55
|
+
if (seen.bytes.size > 1) failures.add('sequence_fork');
|
|
56
|
+
}
|
|
57
|
+
if (!entries.every((entry, index) => index === 0
|
|
58
|
+
|| entries[index - 1]?.[sequenceField] < entry?.[sequenceField])) {
|
|
59
|
+
failures.add('storage_order');
|
|
60
|
+
}
|
|
61
|
+
const sequences = [...bySequence.keys()].sort((left, right) => left - right);
|
|
62
|
+
if (sequences.length === 0 || sequences.some((sequence, index) => sequence !== index)) {
|
|
63
|
+
failures.add('sequence_gap');
|
|
64
|
+
}
|
|
65
|
+
const genesis = bySequence.get(0)?.entries[0];
|
|
66
|
+
if (genesis === undefined || genesis[previousField] !== genesisPrevious
|
|
67
|
+
|| entries.some((entry) => (entry[sequenceField] === 0) !== (entry[previousField] === genesisPrevious))) {
|
|
68
|
+
failures.add('genesis_binding');
|
|
69
|
+
}
|
|
70
|
+
for (let index = 1; index < sequences.length; index += 1) {
|
|
71
|
+
const previous = bySequence.get(sequences[index - 1]).entries[0];
|
|
72
|
+
const current = bySequence.get(sequences[index]).entries[0];
|
|
73
|
+
if (current[previousField] !== previous[digestField]) failures.add('digest_chain_mismatch');
|
|
74
|
+
}
|
|
75
|
+
return failures;
|
|
76
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lattice-mcp bin向けNode versionガード(ADR 0049 wave2レビューでのスコープ外
|
|
3
3
|
* 発見の修理 — bin/lattice-mcp.mjs が sensor/dist/index.js を直import して
|
|
4
|
-
* MCPServer を起動する経路には、sensor CLI (sensor/src/bin/
|
|
4
|
+
* MCPServer を起動する経路には、sensor CLI (sensor/src/bin/sensor.ts) が
|
|
5
5
|
* 持つNode versionガードが一切通っていなかった)。
|
|
6
6
|
*
|
|
7
7
|
* 閾値・banner文言は複製せず、sensor CLI と同じ
|
|
@@ -10,20 +10,21 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 判定ロジックだけをここに切り出し、process.versions.node / env を直接
|
|
12
12
|
* 読まない純関数にすることで、境界値(MIN_NODE_MAJOR-1 / MIN_NODE_MAJOR /
|
|
13
|
-
* 24 / 25、override有無)をunit testで固定できるようにする。
|
|
13
|
+
* 24 / 25 / 26、override有無)をunit testで固定できるようにする。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
MIN_NODE_MAJOR,
|
|
18
18
|
buildNode25BlockBanner,
|
|
19
19
|
buildNodeTooOldBanner,
|
|
20
|
+
isNode25Affected,
|
|
20
21
|
} from '../sensor/dist/bin/node-version-check.js';
|
|
21
22
|
|
|
22
23
|
export { MIN_NODE_MAJOR };
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* @param {string} nodeVersion - process.versions.node相当の文字列 (例: "26.5.0")
|
|
26
|
-
* @param {boolean} overrideActive -
|
|
27
|
+
* @param {boolean} overrideActive - LATTICE_SENSOR_ALLOW_UNSAFE_NODE が設定されているか
|
|
27
28
|
* @returns {{ blocked: boolean, banner: string | null }}
|
|
28
29
|
* banner: 表示すべきbanner文言(sensor CLIと同一文言)。null ならサポート
|
|
29
30
|
* 対象内で表示不要。
|
|
@@ -33,7 +34,7 @@ export { MIN_NODE_MAJOR };
|
|
|
33
34
|
*/
|
|
34
35
|
export function evaluateNodeVersionGuard(nodeVersion, overrideActive) {
|
|
35
36
|
const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
|
|
36
|
-
if (nodeMajor
|
|
37
|
+
if (isNode25Affected(nodeMajor)) {
|
|
37
38
|
return { blocked: !overrideActive, banner: buildNode25BlockBanner(nodeVersion) };
|
|
38
39
|
}
|
|
39
40
|
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
canonicalizeTodoArtifact,
|
|
8
|
+
exactRecord,
|
|
9
|
+
isStrictTodoTimestamp,
|
|
10
|
+
isTodoDigest,
|
|
11
|
+
isTodoIdentifier,
|
|
12
|
+
isTodoRef,
|
|
13
|
+
todoSelfDigest,
|
|
14
|
+
} from './todo-contracts.mjs';
|
|
15
|
+
import { projectTodoStatus } from './todo-status.mjs';
|
|
16
|
+
import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
|
|
17
|
+
import { resolveProjectIdentity } from './project-identity.mjs';
|
|
18
|
+
import {
|
|
19
|
+
buildTodoPlan,
|
|
20
|
+
createTodoStoreWriter,
|
|
21
|
+
initializeAuthoredTodoStore,
|
|
22
|
+
readTodoStore,
|
|
23
|
+
TodoStoreError,
|
|
24
|
+
} from './todo-store.mjs';
|
|
25
|
+
|
|
26
|
+
const STORE_REF = '.lattice/todo';
|
|
27
|
+
const MANIFEST_REF = `${STORE_REF}/manifest.json`;
|
|
28
|
+
const MAX_INPUT_BYTES = 8_388_608;
|
|
29
|
+
const STATUS_SCHEMA = 'lattice.project_status.v1';
|
|
30
|
+
const CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v1';
|
|
31
|
+
const PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v2';
|
|
32
|
+
const DECOUPLED_PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v3';
|
|
33
|
+
const CURRENT_CREATE_INPUT_SCHEMA = DECOUPLED_PHASE_CREATE_INPUT_SCHEMA;
|
|
34
|
+
const CURRENT_CREATE_SCHEMA_COMMAND = 'lattice plan create --schema-version 3 --json';
|
|
35
|
+
const CREATE_RESULT_SCHEMA = 'lattice.plan_create_result.v1';
|
|
36
|
+
|
|
37
|
+
function resolveRepoRoot(cwd) {
|
|
38
|
+
try {
|
|
39
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
40
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
41
|
+
}).replace(/\r?\n$/u, '');
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function gitHead(repoRoot) {
|
|
48
|
+
try {
|
|
49
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
50
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
51
|
+
}).replace(/\r?\n$/u, '');
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resultDigest(value) {
|
|
58
|
+
return todoSelfDigest(value, 'result_digest');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function plain(value) {
|
|
62
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
63
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateProjectStatus(value) {
|
|
67
|
+
try {
|
|
68
|
+
if (!exactRecord(value, [
|
|
69
|
+
'schema', 'cli', 'project', 'state', 'store', 'active_plans', 'active_runs',
|
|
70
|
+
'can_create_plan', 'next_action', 'result_digest',
|
|
71
|
+
]) || value.schema !== STATUS_SCHEMA
|
|
72
|
+
|| !exactRecord(value.cli, ['available', 'version']) || value.cli.available !== true
|
|
73
|
+
|| typeof value.cli.version !== 'string' || value.cli.version.length > 64
|
|
74
|
+
|| !['uninitialized', 'ready', 'active_run', 'invalid'].includes(value.state)
|
|
75
|
+
|| !exactRecord(value.store, ['ref', 'absolute_path']) || value.store.ref !== STORE_REF
|
|
76
|
+
|| (value.store.absolute_path !== null && typeof value.store.absolute_path !== 'string')
|
|
77
|
+
|| !Array.isArray(value.active_plans) || value.active_plans.length > 256
|
|
78
|
+
|| !value.active_plans.every((entry) => exactRecord(entry, ['plan_key', 'plan_version'])
|
|
79
|
+
&& isTodoIdentifier(entry.plan_key) && isTodoIdentifier(entry.plan_version))
|
|
80
|
+
|| !Array.isArray(value.active_runs) || value.active_runs.length > 2_000
|
|
81
|
+
|| !value.active_runs.every((entry) => exactRecord(entry, ['plan_key', 'task_id', 'label'])
|
|
82
|
+
&& isTodoIdentifier(entry.plan_key) && isTodoIdentifier(entry.task_id)
|
|
83
|
+
&& typeof entry.label === 'string' && [...entry.label].length > 0 && [...entry.label].length <= 160)
|
|
84
|
+
|| typeof value.can_create_plan !== 'boolean' || !plain(value.next_action)
|
|
85
|
+
|| typeof value.next_action.command !== 'string' || value.next_action.command.length === 0
|
|
86
|
+
|| !isTodoDigest(value.result_digest) || value.result_digest !== resultDigest(value)) return false;
|
|
87
|
+
if (value.project !== null && (!exactRecord(value.project, ['root', 'git_head', 'project_id'])
|
|
88
|
+
|| typeof value.project.root !== 'string' || value.project.root.length === 0
|
|
89
|
+
|| (value.project.git_head !== null && !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value.project.git_head))
|
|
90
|
+
|| (value.project.project_id !== null && !isTodoIdentifier(value.project.project_id)))) return false;
|
|
91
|
+
if (value.state === 'uninitialized') {
|
|
92
|
+
return value.project !== null && value.project.project_id === null && value.can_create_plan === true
|
|
93
|
+
&& exactRecord(value.next_action, ['command', 'input_schema', 'schema_command'])
|
|
94
|
+
&& value.next_action.input_schema === CURRENT_CREATE_INPUT_SCHEMA
|
|
95
|
+
&& value.next_action.schema_command === CURRENT_CREATE_SCHEMA_COMMAND;
|
|
96
|
+
}
|
|
97
|
+
if (value.state === 'invalid') {
|
|
98
|
+
return value.can_create_plan === false && exactRecord(value.next_action, ['command', 'reason'])
|
|
99
|
+
&& typeof value.next_action.reason === 'string' && value.next_action.reason.length > 0;
|
|
100
|
+
}
|
|
101
|
+
return value.project !== null && isTodoIdentifier(value.project.project_id)
|
|
102
|
+
&& value.can_create_plan === false && exactRecord(value.next_action, ['command', 'reason'])
|
|
103
|
+
&& typeof value.next_action.reason === 'string' && value.next_action.reason.length > 0
|
|
104
|
+
&& (value.state !== 'active_run' || value.active_runs.length > 0)
|
|
105
|
+
&& (value.state !== 'ready' || value.active_runs.length === 0);
|
|
106
|
+
} catch { return false; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function statusResult(fields) {
|
|
110
|
+
const result = { schema: STATUS_SCHEMA, ...fields, result_digest: '' };
|
|
111
|
+
result.result_digest = resultDigest(result);
|
|
112
|
+
if (!validateProjectStatus(result)) throw new TypeError('project status result contract invalid');
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function invalidStatus({ cliVersion, repoRoot, reason }) {
|
|
117
|
+
return statusResult({
|
|
118
|
+
cli: { available: true, version: cliVersion },
|
|
119
|
+
project: repoRoot === null ? null : { root: repoRoot, git_head: gitHead(repoRoot), project_id: null },
|
|
120
|
+
state: 'invalid',
|
|
121
|
+
store: { ref: STORE_REF, absolute_path: repoRoot === null ? null : path.join(repoRoot, STORE_REF) },
|
|
122
|
+
active_plans: [], active_runs: [], can_create_plan: false,
|
|
123
|
+
next_action: { command: repoRoot === null ? 'git status' : 'lattice todo verify', reason },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.env,
|
|
128
|
+
ensureDashboardActivity = ensureTodoDashboardActivity }) {
|
|
129
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
130
|
+
if (repoRoot === null) {
|
|
131
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'git_repository_unresolved' }))}\n`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const storeAbsolute = path.join(repoRoot, STORE_REF);
|
|
135
|
+
const manifestAbsolute = path.join(repoRoot, MANIFEST_REF);
|
|
136
|
+
const latticeAbsolute = path.join(repoRoot, '.lattice');
|
|
137
|
+
let latticeState;
|
|
138
|
+
let storeState;
|
|
139
|
+
let manifestState;
|
|
140
|
+
try { latticeState = await lstat(latticeAbsolute); } catch (error) {
|
|
141
|
+
if (error?.code !== 'ENOENT') {
|
|
142
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'lattice_root_unreadable' }))}\n`);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (latticeState !== undefined && (latticeState.isSymbolicLink() || !latticeState.isDirectory())) {
|
|
147
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'lattice_root_invalid' }))}\n`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
try { storeState = await lstat(storeAbsolute); } catch (error) {
|
|
151
|
+
if (error?.code !== 'ENOENT') {
|
|
152
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'store_unreadable' }))}\n`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
try { manifestState = await lstat(manifestAbsolute); } catch (error) {
|
|
157
|
+
if (error?.code !== 'ENOENT') {
|
|
158
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'manifest_unreadable' }))}\n`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (storeState === undefined && manifestState === undefined) {
|
|
163
|
+
const result = statusResult({
|
|
164
|
+
cli: { available: true, version: cliVersion },
|
|
165
|
+
project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: null },
|
|
166
|
+
state: 'uninitialized',
|
|
167
|
+
store: { ref: STORE_REF, absolute_path: storeAbsolute },
|
|
168
|
+
active_plans: [], active_runs: [], can_create_plan: true,
|
|
169
|
+
next_action: {
|
|
170
|
+
command: 'lattice plan create --input .lattice/plan-create.json',
|
|
171
|
+
input_schema: CURRENT_CREATE_INPUT_SCHEMA,
|
|
172
|
+
schema_command: CURRENT_CREATE_SCHEMA_COMMAND,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
if (storeState?.isSymbolicLink() || !storeState?.isDirectory()
|
|
179
|
+
|| manifestState?.isSymbolicLink() || !manifestState?.isFile()) {
|
|
180
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'store_layout_invalid' }))}\n`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const store = await readTodoStore({ repoRoot });
|
|
185
|
+
const todo = projectTodoStatus(store);
|
|
186
|
+
const activeRuns = todo.active_set.map((entry) => ({
|
|
187
|
+
plan_key: entry.plan_key, task_id: entry.task_id, label: entry.label,
|
|
188
|
+
}));
|
|
189
|
+
const state = activeRuns.length > 0 ? 'active_run' : 'ready';
|
|
190
|
+
const next = activeRuns.length > 0
|
|
191
|
+
? { command: 'lattice todo status', reason: 'active_run_present' }
|
|
192
|
+
: todo.next_ready.length > 0
|
|
193
|
+
? { command: `lattice todo start --plan ${todo.next_ready[0].plan_key} --task ${todo.next_ready[0].task_id}${todo.next_ready.length > 1 ? ' --parallel-frontier' : ''}`,
|
|
194
|
+
reason: todo.next_ready.length > 1 ? 'parallel_frontier_present' : 'next_ready_present' }
|
|
195
|
+
: { command: 'lattice todo status', reason: 'no_ready_task' };
|
|
196
|
+
const result = statusResult({
|
|
197
|
+
cli: { available: true, version: cliVersion },
|
|
198
|
+
project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: store.project_id },
|
|
199
|
+
state,
|
|
200
|
+
store: { ref: STORE_REF, absolute_path: storeAbsolute },
|
|
201
|
+
active_plans: todo.member_heads.map((entry) => ({
|
|
202
|
+
plan_key: entry.plan_key, plan_version: entry.plan_version,
|
|
203
|
+
})),
|
|
204
|
+
active_runs: activeRuns,
|
|
205
|
+
can_create_plan: false,
|
|
206
|
+
next_action: next,
|
|
207
|
+
});
|
|
208
|
+
if (env.LATTICE_DASHBOARD_AUTOSTART !== '0') {
|
|
209
|
+
const identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env });
|
|
210
|
+
const actorSession = env.LATTICE_TODO_ACTOR_SESSION;
|
|
211
|
+
await ensureDashboardActivity({
|
|
212
|
+
repoRoot, projectId: store.project_id, displayName: identity.displayName,
|
|
213
|
+
sessionId: isTodoIdentifier(actorSession) ? actorSession : `status-${process.pid}`, env,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
217
|
+
return 0;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const reason = error instanceof TodoStoreError ? `${error.code}:${error.detail?.reason ?? error.message}` : 'store_validation_failed';
|
|
220
|
+
stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason }))}\n`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function readCanonicalInput(repoRoot, inputRef) {
|
|
226
|
+
if (!isTodoRef(inputRef)) throw new TodoStoreError('INPUT_INVALID', 'input_ref_invalid');
|
|
227
|
+
const root = await realpath(repoRoot);
|
|
228
|
+
const absolute = path.resolve(root, inputRef);
|
|
229
|
+
if (!absolute.startsWith(`${root}${path.sep}`)) throw new TodoStoreError('INPUT_INVALID', 'input_outside_repo');
|
|
230
|
+
let cursor = root;
|
|
231
|
+
try {
|
|
232
|
+
for (const part of path.relative(root, absolute).split(path.sep)) {
|
|
233
|
+
cursor = path.join(cursor, part);
|
|
234
|
+
const entry = await lstat(cursor);
|
|
235
|
+
if (entry.isSymbolicLink()) throw new TodoStoreError('INPUT_INVALID', 'input_path_symlink');
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof TodoStoreError) throw error;
|
|
239
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_unreadable');
|
|
240
|
+
}
|
|
241
|
+
const state = await lstat(absolute);
|
|
242
|
+
if (!state.isFile() || await realpath(absolute) !== absolute) {
|
|
243
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_path_invalid');
|
|
244
|
+
}
|
|
245
|
+
if (state.size > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
246
|
+
let handle;
|
|
247
|
+
let bytes;
|
|
248
|
+
try {
|
|
249
|
+
handle = await open(absolute, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
250
|
+
const opened = await handle.stat();
|
|
251
|
+
if (opened.dev !== state.dev || opened.ino !== state.ino || opened.size !== state.size
|
|
252
|
+
|| opened.mtimeMs !== state.mtimeMs || opened.ctimeMs !== state.ctimeMs || !opened.isFile()) {
|
|
253
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_changed_during_validation');
|
|
254
|
+
}
|
|
255
|
+
if (opened.size > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
256
|
+
const capture = Buffer.allocUnsafe(MAX_INPUT_BYTES + 1);
|
|
257
|
+
let captured = 0;
|
|
258
|
+
while (captured < capture.length) {
|
|
259
|
+
const { bytesRead } = await handle.read(capture, captured, capture.length - captured, null);
|
|
260
|
+
if (bytesRead === 0) break;
|
|
261
|
+
captured += bytesRead;
|
|
262
|
+
}
|
|
263
|
+
if (captured > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
264
|
+
bytes = capture.subarray(0, captured);
|
|
265
|
+
const after = await lstat(absolute);
|
|
266
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size
|
|
267
|
+
|| after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs
|
|
268
|
+
|| await realpath(absolute) !== absolute) {
|
|
269
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_changed_during_validation');
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error instanceof TodoStoreError) throw error;
|
|
273
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_unreadable');
|
|
274
|
+
} finally {
|
|
275
|
+
await handle?.close();
|
|
276
|
+
}
|
|
277
|
+
if (bytes.length > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
278
|
+
let text;
|
|
279
|
+
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
280
|
+
catch { throw new TodoStoreError('INPUT_INVALID', 'input_utf8_invalid'); }
|
|
281
|
+
if (!text.endsWith('\n') || text.includes('\r') || text.slice(0, -1).includes('\n')) {
|
|
282
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_bytes_noncanonical');
|
|
283
|
+
}
|
|
284
|
+
let value;
|
|
285
|
+
try { value = JSON.parse(text.slice(0, -1)); } catch { throw new TodoStoreError('INPUT_INVALID', 'input_json_invalid'); }
|
|
286
|
+
if (`${canonicalizeTodoArtifact(value)}\n` !== text) throw new TodoStoreError('INPUT_INVALID', 'input_bytes_noncanonical');
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function validateCreateInput(value) {
|
|
291
|
+
const phaseInput = [PHASE_CREATE_INPUT_SCHEMA, DECOUPLED_PHASE_CREATE_INPUT_SCHEMA].includes(value?.schema);
|
|
292
|
+
const decoupledPhaseInput = value?.schema === DECOUPLED_PHASE_CREATE_INPUT_SCHEMA;
|
|
293
|
+
const keys = [
|
|
294
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
295
|
+
'tasks', 'hard_dependencies', 'joins', 'input_digest',
|
|
296
|
+
];
|
|
297
|
+
if (phaseInput) keys.push('phases');
|
|
298
|
+
if (decoupledPhaseInput) keys.push('phase_accept_dependencies');
|
|
299
|
+
if (!exactRecord(value, keys) || ![
|
|
300
|
+
CREATE_INPUT_SCHEMA, PHASE_CREATE_INPUT_SCHEMA, DECOUPLED_PHASE_CREATE_INPUT_SCHEMA,
|
|
301
|
+
].includes(value.schema)
|
|
302
|
+
|| !isTodoIdentifier(value.project_id)
|
|
303
|
+
|| !isTodoIdentifier(value.plan_key) || !isTodoIdentifier(value.plan_version)
|
|
304
|
+
|| !exactRecord(value.actor, ['host', 'session', 'agent'])
|
|
305
|
+
|| ![value.actor.host, value.actor.session, value.actor.agent].every(isTodoIdentifier)
|
|
306
|
+
|| !isStrictTodoTimestamp(value.recorded_at) || !isTodoDigest(value.input_digest)
|
|
307
|
+
|| value.input_digest !== todoSelfDigest(value, 'input_digest')
|
|
308
|
+
|| !Array.isArray(value.tasks)
|
|
309
|
+
|| value.tasks.some((task) => task?.narrative_anchor !== null || task?.compile_binding !== null)) return false;
|
|
310
|
+
try {
|
|
311
|
+
buildTodoPlan({
|
|
312
|
+
schema: decoupledPhaseInput ? 'lattice.todo_plan.v5'
|
|
313
|
+
: phaseInput ? 'lattice.todo_plan.v4' : 'lattice.todo_plan.v3', project_id: value.project_id,
|
|
314
|
+
plan_key: value.plan_key, plan_version: value.plan_version,
|
|
315
|
+
predecessor_plan_digest: null, tasks: value.tasks,
|
|
316
|
+
hard_dependencies: value.hard_dependencies, joins: value.joins,
|
|
317
|
+
...(phaseInput ? { phases: value.phases } : {}),
|
|
318
|
+
...(decoupledPhaseInput ? { phase_accept_dependencies: value.phase_accept_dependencies } : {}),
|
|
319
|
+
});
|
|
320
|
+
return true;
|
|
321
|
+
} catch { return false; }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function runPlanCreate({ cwd, inputRef, stdout }) {
|
|
325
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
326
|
+
if (repoRoot === null) throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved');
|
|
327
|
+
const input = await readCanonicalInput(repoRoot, inputRef);
|
|
328
|
+
if (!validateCreateInput(input)) throw new TodoStoreError('INPUT_INVALID', 'plan_create_schema_invalid');
|
|
329
|
+
const store = await initializeAuthoredTodoStore({
|
|
330
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
331
|
+
projectId: input.project_id, repositories: [{ repo_id: 'self', path: '.' }],
|
|
332
|
+
plan: {
|
|
333
|
+
schema: input.schema === DECOUPLED_PHASE_CREATE_INPUT_SCHEMA ? 'lattice.todo_plan.v5'
|
|
334
|
+
: input.schema === PHASE_CREATE_INPUT_SCHEMA
|
|
335
|
+
? 'lattice.todo_plan.v4' : 'lattice.todo_plan.v3', project_id: input.project_id,
|
|
336
|
+
plan_key: input.plan_key, plan_version: input.plan_version,
|
|
337
|
+
predecessor_plan_digest: null, tasks: input.tasks,
|
|
338
|
+
hard_dependencies: input.hard_dependencies, joins: input.joins,
|
|
339
|
+
...(input.schema === PHASE_CREATE_INPUT_SCHEMA
|
|
340
|
+
|| input.schema === DECOUPLED_PHASE_CREATE_INPUT_SCHEMA ? { phases: input.phases } : {}),
|
|
341
|
+
...(input.schema === DECOUPLED_PHASE_CREATE_INPUT_SCHEMA
|
|
342
|
+
? { phase_accept_dependencies: input.phase_accept_dependencies } : {}),
|
|
343
|
+
},
|
|
344
|
+
genesis: { actor: input.actor, recorded_at: input.recorded_at, provenance: null },
|
|
345
|
+
});
|
|
346
|
+
const member = store.members[0];
|
|
347
|
+
const result = {
|
|
348
|
+
schema: CREATE_RESULT_SCHEMA, project_id: store.project_id,
|
|
349
|
+
plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
|
|
350
|
+
store_ref: STORE_REF, plan_ref: member.descriptor.plan_ref,
|
|
351
|
+
journal_ref: member.descriptor.journal_ref, snapshot_ref: member.descriptor.snapshot_ref,
|
|
352
|
+
plan_digest: member.plan.plan_digest, result_digest: '',
|
|
353
|
+
};
|
|
354
|
+
result.result_digest = resultDigest(result);
|
|
355
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function runPlanCreateSchema({ stdout, version = 1 }) {
|
|
360
|
+
if (![1, 2, 3].includes(version)) throw new TypeError('unsupported plan create schema version');
|
|
361
|
+
const expected = version === 3 ? DECOUPLED_PHASE_CREATE_INPUT_SCHEMA
|
|
362
|
+
: version === 2 ? PHASE_CREATE_INPUT_SCHEMA : CREATE_INPUT_SCHEMA;
|
|
363
|
+
const schemaUrl = new URL(`../docs/schemas/lattice.plan_create_input.v${version}.schema.json`, import.meta.url);
|
|
364
|
+
const handle = await open(schemaUrl, fsConstants.O_RDONLY);
|
|
365
|
+
try {
|
|
366
|
+
const schema = JSON.parse(await handle.readFile('utf8'));
|
|
367
|
+
if (schema?.title !== expected) throw new TypeError('bundled plan create schema invalid');
|
|
368
|
+
stdout.write(`${JSON.stringify(schema)}\n`);
|
|
369
|
+
return 0;
|
|
370
|
+
} finally {
|
|
371
|
+
await handle.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function projectStatusFailure({ cwd, stdout, cliVersion, error }) {
|
|
376
|
+
const result = invalidStatus({
|
|
377
|
+
cliVersion, repoRoot: resolveRepoRoot(cwd),
|
|
378
|
+
reason: `status_internal_failure:${error?.constructor?.name ?? 'Error'}`,
|
|
379
|
+
});
|
|
380
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function projectCliFailure(stderr, error) {
|
|
385
|
+
const payload = {
|
|
386
|
+
schema: 'lattice.cli_error.v2', code: error?.code ?? 'INTERNAL_FAILURE',
|
|
387
|
+
message: error?.message ?? 'project command failed',
|
|
388
|
+
};
|
|
389
|
+
if (error?.detail && Object.keys(error.detail).length > 0) payload.detail = error.detail;
|
|
390
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseTree } from 'jsonc-parser';
|
|
4
|
+
|
|
5
|
+
export const PROJECT_IDENTITY_REF = '.lattice/project.json';
|
|
6
|
+
const MAX_BYTES = 65_536;
|
|
7
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
8
|
+
|
|
9
|
+
function fail(reason) {
|
|
10
|
+
const error = new Error(reason);
|
|
11
|
+
error.code = 'PROJECT_IDENTITY_INVALID';
|
|
12
|
+
error.detail = { reason };
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function displayName(value) {
|
|
17
|
+
return typeof value === 'string' && value.length > 0 && value === value.trim()
|
|
18
|
+
&& !CONTROL.test(value) && Buffer.byteLength(value, 'utf8') <= 256;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function duplicateKey(node) {
|
|
22
|
+
if (node?.type === 'object') {
|
|
23
|
+
const keys = new Set();
|
|
24
|
+
for (const property of node.children ?? []) {
|
|
25
|
+
const [key, value] = property.children ?? [];
|
|
26
|
+
if (keys.has(key?.value) || duplicateKey(value)) return true;
|
|
27
|
+
keys.add(key?.value);
|
|
28
|
+
}
|
|
29
|
+
} else if (node?.type === 'array') return (node.children ?? []).some(duplicateKey);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveProjectIdentity({ repoRoot, projectId, env = process.env }) {
|
|
34
|
+
if (!path.isAbsolute(repoRoot) || typeof projectId !== 'string' || projectId.length === 0
|
|
35
|
+
|| env === null || typeof env !== 'object' || Array.isArray(env)) {
|
|
36
|
+
throw new TypeError('project identity options invalid');
|
|
37
|
+
}
|
|
38
|
+
const override = env.LATTICE_PROJECT_DISPLAY_NAME;
|
|
39
|
+
if (override !== undefined) {
|
|
40
|
+
if (!displayName(override)) fail('environment_display_name_invalid');
|
|
41
|
+
return Object.freeze({ projectId, displayName: override, source: 'environment' });
|
|
42
|
+
}
|
|
43
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
44
|
+
const ref = path.join(canonicalRoot, PROJECT_IDENTITY_REF);
|
|
45
|
+
let stats;
|
|
46
|
+
try { stats = await lstat(ref); } catch (error) {
|
|
47
|
+
if (error?.code === 'ENOENT') {
|
|
48
|
+
return Object.freeze({ projectId, displayName: projectId, source: 'project_id' });
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_BYTES) fail('identity_file_unsafe');
|
|
53
|
+
const resolved = await realpath(ref);
|
|
54
|
+
if (resolved !== ref) fail('identity_file_alias');
|
|
55
|
+
const bytes = await readFile(ref);
|
|
56
|
+
if (bytes.length > MAX_BYTES) fail('identity_file_too_large');
|
|
57
|
+
let text;
|
|
58
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { fail('identity_utf8_invalid'); }
|
|
59
|
+
const errors = [];
|
|
60
|
+
const tree = parseTree(text, errors, { allowTrailingComma: false, disallowComments: true });
|
|
61
|
+
if (errors.length > 0 || tree === undefined || duplicateKey(tree)) fail('identity_json_invalid');
|
|
62
|
+
let document;
|
|
63
|
+
try { document = JSON.parse(text); } catch { fail('identity_json_invalid'); }
|
|
64
|
+
if (document === null || typeof document !== 'object' || Array.isArray(document)
|
|
65
|
+
|| Object.keys(document).sort().join(',') !== 'display_name,project_id,schema'
|
|
66
|
+
|| document.schema !== 'lattice.project_identity.v1'
|
|
67
|
+
|| document.project_id !== projectId || !displayName(document.display_name)) {
|
|
68
|
+
fail('identity_schema_invalid');
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze({ projectId, displayName: document.display_name, source: 'project_file' });
|
|
71
|
+
}
|