@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,269 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
|
|
3
|
+
const LOOPBACK = '127.0.0.1';
|
|
4
|
+
const POLL_MS = 500;
|
|
5
|
+
const HTTP_ERROR_SCHEMA = 'lattice.todo_gantt_http_error.v1';
|
|
6
|
+
const PROJECT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
7
|
+
|
|
8
|
+
function projectPath(projectId) {
|
|
9
|
+
return `/projects/${encodeURIComponent(projectId)}/`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function liveHtml(html, headDigest, eventsPath) {
|
|
13
|
+
const controller = `<script>(()=>{const badge=document.createElement('div');badge.setAttribute('role','status');badge.style.cssText='position:fixed;right:12px;bottom:12px;z-index:99;padding:6px 10px;border:1px solid #d9d8d4;border-radius:4px;background:#fcfcfb;font:600 12px system-ui';badge.textContent='進捗: 接続中';document.body.append(badge);let head=${JSON.stringify(headDigest)};const stream=new EventSource(${JSON.stringify(eventsPath)});stream.addEventListener('state',event=>{const next=JSON.parse(event.data);badge.textContent='進捗: 最新';if(next.head_digest!==head){badge.textContent='進捗: 更新を反映中';location.reload();}});stream.addEventListener('lattice-error',event=>{const detail=JSON.parse(event.data);badge.textContent='進捗: エラー '+detail.code;badge.style.borderColor='#d03b3b';});stream.onerror=()=>{badge.textContent='進捗: 再接続中';};})();</script>`;
|
|
14
|
+
return html
|
|
15
|
+
.replace("default-src 'none';", "default-src 'none'; connect-src 'self';")
|
|
16
|
+
.replace('</body>', `${controller}</body>`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function escapeHtml(value) {
|
|
20
|
+
return String(value).replaceAll('&', '&').replaceAll('<', '<')
|
|
21
|
+
.replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function dashboardHtml(projects) {
|
|
25
|
+
const rows = projects.map(({ projectId, displayName }) => {
|
|
26
|
+
const href = projectPath(projectId);
|
|
27
|
+
return `<li><a href="${escapeHtml(href)}"><strong>${escapeHtml(displayName)}</strong><code>${escapeHtml(projectId)}</code></a></li>`;
|
|
28
|
+
}).join('');
|
|
29
|
+
const content = rows.length === 0 ? '<p>アクティブなプロジェクトはありません。</p>'
|
|
30
|
+
: `<ul>${rows}</ul>`;
|
|
31
|
+
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — アクティブプロジェクト</title><style>body{max-width:800px;margin:48px auto;padding:0 20px;color:#262626;background:#fafaf8;font:16px system-ui}h1{font-size:1.6rem}ul{list-style:none;padding:0}li{margin:10px 0}a{display:flex;justify-content:space-between;gap:20px;padding:16px;border:1px solid #d9d8d4;border-radius:8px;color:inherit;background:white;text-decoration:none}a:hover{border-color:#4b78d0}code{color:#666}</style></head><body><main><h1>Lattice アクティブプロジェクト</h1>${content}</main></body></html>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateProject(project) {
|
|
35
|
+
if (project === null || typeof project !== 'object' || Array.isArray(project)
|
|
36
|
+
|| typeof project.projectId !== 'string' || !PROJECT_ID.test(project.projectId)
|
|
37
|
+
|| typeof project.displayName !== 'string' || project.displayName.length === 0
|
|
38
|
+
|| project.displayName !== project.displayName.trim()
|
|
39
|
+
|| typeof project.render !== 'function' || typeof project.readHead !== 'function') {
|
|
40
|
+
throw new TypeError('project descriptor is invalid');
|
|
41
|
+
}
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
projectId: project.projectId,
|
|
44
|
+
displayName: project.displayName,
|
|
45
|
+
render: project.render,
|
|
46
|
+
readHead: project.readHead,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createTodoGanttProjectRegistry(initialProjects = []) {
|
|
51
|
+
if (!Array.isArray(initialProjects)) throw new TypeError('initialProjects must be an array');
|
|
52
|
+
const entries = new Map();
|
|
53
|
+
const listeners = new Set();
|
|
54
|
+
const notify = () => {
|
|
55
|
+
for (const listener of listeners) listener();
|
|
56
|
+
};
|
|
57
|
+
const register = (project) => {
|
|
58
|
+
const validated = validateProject(project);
|
|
59
|
+
if (entries.has(validated.projectId)) {
|
|
60
|
+
const error = new Error(`project already registered: ${validated.projectId}`);
|
|
61
|
+
error.code = 'PROJECT_ALREADY_REGISTERED';
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
entries.set(validated.projectId, validated);
|
|
65
|
+
notify();
|
|
66
|
+
return validated;
|
|
67
|
+
};
|
|
68
|
+
for (const project of initialProjects) register(project);
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
register,
|
|
71
|
+
unregister(projectId) {
|
|
72
|
+
const deleted = entries.delete(projectId);
|
|
73
|
+
if (deleted) notify();
|
|
74
|
+
return deleted;
|
|
75
|
+
},
|
|
76
|
+
get(projectId) { return entries.get(projectId) ?? null; },
|
|
77
|
+
list() {
|
|
78
|
+
return [...entries.values()].sort((left, right) => left.displayName.localeCompare(right.displayName, 'ja')
|
|
79
|
+
|| left.projectId.localeCompare(right.projectId, 'en'));
|
|
80
|
+
},
|
|
81
|
+
subscribe(listener) {
|
|
82
|
+
if (typeof listener !== 'function') throw new TypeError('registry listener required');
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
return () => listeners.delete(listener);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sendEvent(response, name, id, value) {
|
|
90
|
+
response.write(`id: ${id}\nevent: ${name}\ndata: ${JSON.stringify(value)}\n\n`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sendHttpError(response, status, code, path) {
|
|
94
|
+
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8',
|
|
95
|
+
'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
96
|
+
response.end(`${JSON.stringify({ schema: HTTP_ERROR_SCHEMA, code, path })}\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function finishRequestFailure(response, code, path) {
|
|
100
|
+
try {
|
|
101
|
+
if (!response.headersSent) sendHttpError(response, 500, code, path);
|
|
102
|
+
else if (!response.writableEnded) response.end();
|
|
103
|
+
} catch { response.destroy(); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function startTodoGanttDashboardServer({ registry, port = 0, redirectSingleProject = false }) {
|
|
107
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65_535) throw new TypeError('port must be 0..65535');
|
|
108
|
+
if (registry === null || typeof registry !== 'object' || typeof registry.get !== 'function'
|
|
109
|
+
|| typeof registry.list !== 'function') throw new TypeError('registry required');
|
|
110
|
+
const clientsByProject = new Map();
|
|
111
|
+
const lastHeads = new Map();
|
|
112
|
+
let eventId = 0;
|
|
113
|
+
let checking = false;
|
|
114
|
+
let closed = false;
|
|
115
|
+
const reconcileClients = () => {
|
|
116
|
+
const active = new Set(registry.list().map(({ projectId }) => projectId));
|
|
117
|
+
for (const [projectId, clients] of clientsByProject) {
|
|
118
|
+
if (active.has(projectId)) continue;
|
|
119
|
+
for (const client of clients) client.end();
|
|
120
|
+
clientsByProject.delete(projectId);
|
|
121
|
+
lastHeads.delete(projectId);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const unsubscribe = typeof registry.subscribe === 'function'
|
|
125
|
+
? registry.subscribe(reconcileClients) : () => {};
|
|
126
|
+
const handleRequest = async (request, response) => {
|
|
127
|
+
let requestPath = request.url ?? '/';
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(requestPath, `http://${LOOPBACK}`);
|
|
130
|
+
requestPath = url.pathname;
|
|
131
|
+
if (request.method !== 'GET') {
|
|
132
|
+
response.setHeader('allow', 'GET');
|
|
133
|
+
sendHttpError(response, 405, 'METHOD_NOT_ALLOWED', requestPath);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (url.pathname === '/__lattice/health') {
|
|
137
|
+
response.writeHead(200, { 'content-type': 'application/json; charset=utf-8',
|
|
138
|
+
'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
139
|
+
response.end(`${JSON.stringify({ schema: 'lattice.todo_dashboard_health.v1', pid: process.pid,
|
|
140
|
+
port: actualPort, project_ids: registry.list().map(({ projectId }) => projectId) })}\n`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (url.pathname === '/' || url.pathname === '/projects/') {
|
|
144
|
+
const projects = registry.list();
|
|
145
|
+
if (url.pathname === '/' && redirectSingleProject && projects.length === 1) {
|
|
146
|
+
response.writeHead(302, { location: projectPath(projects[0].projectId), 'cache-control': 'no-store' });
|
|
147
|
+
response.end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8',
|
|
151
|
+
'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
152
|
+
response.end(dashboardHtml(projects));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const match = /^\/projects\/([^/]+)\/(events)?$/u.exec(url.pathname);
|
|
156
|
+
if (match === null) {
|
|
157
|
+
sendHttpError(response, 404, 'ROUTE_NOT_FOUND', url.pathname);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let requestedId;
|
|
161
|
+
try { requestedId = decodeURIComponent(match[1]); } catch {
|
|
162
|
+
sendHttpError(response, 400, 'PROJECT_ID_INVALID', url.pathname);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const project = registry.get(requestedId);
|
|
166
|
+
if (project === null) {
|
|
167
|
+
sendHttpError(response, 404, 'PROJECT_NOT_FOUND', url.pathname);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (match[2] === 'events') {
|
|
171
|
+
response.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8',
|
|
172
|
+
'cache-control': 'no-store', connection: 'keep-alive', 'x-content-type-options': 'nosniff' });
|
|
173
|
+
if (!clientsByProject.has(project.projectId)) clientsByProject.set(project.projectId, new Set());
|
|
174
|
+
clientsByProject.get(project.projectId).add(response);
|
|
175
|
+
request.on('close', () => clientsByProject.get(project.projectId)?.delete(response));
|
|
176
|
+
try {
|
|
177
|
+
const head = await project.readHead();
|
|
178
|
+
lastHeads.set(project.projectId, head);
|
|
179
|
+
sendEvent(response, 'state', ++eventId, { head_digest: head });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
sendEvent(response, 'lattice-error', ++eventId, { code: error?.code ?? 'STORE_READ_FAILED' });
|
|
182
|
+
clientsByProject.get(project.projectId)?.delete(response);
|
|
183
|
+
response.end();
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const rendered = await project.render({
|
|
188
|
+
projectId: project.projectId,
|
|
189
|
+
displayName: project.displayName,
|
|
190
|
+
});
|
|
191
|
+
lastHeads.set(project.projectId, rendered.head_digest);
|
|
192
|
+
const html = liveHtml(rendered.html, rendered.head_digest, `${projectPath(project.projectId)}events`);
|
|
193
|
+
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8',
|
|
194
|
+
'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
195
|
+
response.end(html);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
finishRequestFailure(response, error?.code ?? 'GANTT_RENDER_FAILED', requestPath);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const server = createServer((request, response) => {
|
|
201
|
+
handleRequest(request, response).catch((error) => {
|
|
202
|
+
finishRequestFailure(response, error?.code ?? 'GANTT_REQUEST_FAILED', request.url ?? '/');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
await new Promise((resolve, reject) => {
|
|
206
|
+
server.once('error', reject);
|
|
207
|
+
server.listen({ host: LOOPBACK, port }, resolve);
|
|
208
|
+
});
|
|
209
|
+
const timer = setInterval(async () => {
|
|
210
|
+
if (checking) return;
|
|
211
|
+
checking = true;
|
|
212
|
+
try {
|
|
213
|
+
reconcileClients();
|
|
214
|
+
for (const project of registry.list()) {
|
|
215
|
+
const clients = clientsByProject.get(project.projectId);
|
|
216
|
+
if (clients === undefined || clients.size === 0) continue;
|
|
217
|
+
try {
|
|
218
|
+
const head = await project.readHead();
|
|
219
|
+
if (lastHeads.get(project.projectId) !== head) {
|
|
220
|
+
lastHeads.set(project.projectId, head);
|
|
221
|
+
for (const client of clients) sendEvent(client, 'state', ++eventId, { head_digest: head });
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
for (const client of clients) sendEvent(client, 'lattice-error', ++eventId,
|
|
225
|
+
{ code: error?.code ?? 'STORE_READ_FAILED' });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} finally { checking = false; }
|
|
229
|
+
}, POLL_MS);
|
|
230
|
+
timer.unref();
|
|
231
|
+
const address = server.address();
|
|
232
|
+
const actualPort = typeof address === 'object' && address !== null ? address.port : port;
|
|
233
|
+
const close = async () => {
|
|
234
|
+
if (closed) return;
|
|
235
|
+
closed = true;
|
|
236
|
+
clearInterval(timer);
|
|
237
|
+
unsubscribe();
|
|
238
|
+
for (const clients of clientsByProject.values()) for (const client of clients) client.end();
|
|
239
|
+
clientsByProject.clear();
|
|
240
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
241
|
+
};
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
host: LOOPBACK,
|
|
244
|
+
port: actualPort,
|
|
245
|
+
url: `http://${LOOPBACK}:${actualPort}/`,
|
|
246
|
+
projectsUrl: `http://${LOOPBACK}:${actualPort}/projects/`,
|
|
247
|
+
registry,
|
|
248
|
+
close,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function startTodoGanttLiveServer({ projectId, displayName = projectId, port = 0, render, readHead }) {
|
|
253
|
+
const registry = createTodoGanttProjectRegistry([{ projectId, displayName, render, readHead }]);
|
|
254
|
+
const dashboard = await startTodoGanttDashboardServer({ registry, port, redirectSingleProject: true });
|
|
255
|
+
const path = projectPath(projectId);
|
|
256
|
+
return Object.freeze({
|
|
257
|
+
projectId,
|
|
258
|
+
displayName,
|
|
259
|
+
host: dashboard.host,
|
|
260
|
+
port: dashboard.port,
|
|
261
|
+
projectPath: path,
|
|
262
|
+
eventsPath: `${path}events`,
|
|
263
|
+
url: `http://${dashboard.host}:${dashboard.port}${path}`,
|
|
264
|
+
eventsUrl: `http://${dashboard.host}:${dashboard.port}${path}events`,
|
|
265
|
+
projectsUrl: dashboard.projectsUrl,
|
|
266
|
+
registry,
|
|
267
|
+
close: dashboard.close,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstat, readFile, realpath,
|
|
3
|
+
} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parseTree } from 'jsonc-parser';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
digestTodoArtifact,
|
|
9
|
+
exactRecord,
|
|
10
|
+
isTodoIdentifier,
|
|
11
|
+
} from './todo-contracts.mjs';
|
|
12
|
+
import { TodoStoreError } from './todo-store.mjs';
|
|
13
|
+
|
|
14
|
+
export const TODO_GANTT_PRESENTATION_REF = '.lattice/todo/gantt-presentation.json';
|
|
15
|
+
export const TODO_GANTT_PRESENTATION_SCHEMA = 'lattice.todo_gantt_presentation.v1';
|
|
16
|
+
export const TODO_GANTT_PRESENTATION_MODEL_SCHEMA = 'lattice.todo_gantt_presentation_model.v1';
|
|
17
|
+
|
|
18
|
+
const MAX_PRESENTATION_BYTES = 64 * 1_024;
|
|
19
|
+
const MAX_PLANS = 256;
|
|
20
|
+
const MAX_LANES_PER_PLAN = 256;
|
|
21
|
+
const MAX_NAME_BYTES = 256;
|
|
22
|
+
const MAX_DESCRIPTION_BYTES = 2_048;
|
|
23
|
+
const MAX_TASK_NUMBER_DIGITS = 12;
|
|
24
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
25
|
+
const NUMERIC_SUFFIX = /(\d+)$/u;
|
|
26
|
+
|
|
27
|
+
function fail(reason, detail = {}) {
|
|
28
|
+
throw new TodoStoreError('PRESENTATION_INVALID', reason, undefined, detail);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareText(left, right) {
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function within(root, candidate) {
|
|
36
|
+
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function boundedText(value, maximumBytes) {
|
|
40
|
+
return typeof value === 'string' && value.length > 0 && value === value.trim()
|
|
41
|
+
&& !CONTROL.test(value) && Buffer.byteLength(value, 'utf8') <= maximumBytes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasDuplicateJsonKey(node) {
|
|
45
|
+
if (node?.type === 'object') {
|
|
46
|
+
const keys = new Set();
|
|
47
|
+
for (const property of node.children ?? []) {
|
|
48
|
+
const [key, value] = property.children ?? [];
|
|
49
|
+
if (keys.has(key?.value) || hasDuplicateJsonKey(value)) return true;
|
|
50
|
+
keys.add(key?.value);
|
|
51
|
+
}
|
|
52
|
+
} else if (node?.type === 'array') {
|
|
53
|
+
return (node.children ?? []).some(hasDuplicateJsonKey);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateDocument(value) {
|
|
59
|
+
if (!exactRecord(value, ['schema', 'project_id', 'plans'])
|
|
60
|
+
|| value.schema !== TODO_GANTT_PRESENTATION_SCHEMA
|
|
61
|
+
|| !isTodoIdentifier(value.project_id)
|
|
62
|
+
|| !Array.isArray(value.plans) || value.plans.length > MAX_PLANS) {
|
|
63
|
+
fail('presentation_schema_invalid');
|
|
64
|
+
}
|
|
65
|
+
const planKeys = new Set();
|
|
66
|
+
for (const plan of value.plans) {
|
|
67
|
+
if (!exactRecord(plan, ['plan_key', 'lanes']) || !isTodoIdentifier(plan.plan_key)
|
|
68
|
+
|| !Array.isArray(plan.lanes) || plan.lanes.length > MAX_LANES_PER_PLAN) {
|
|
69
|
+
fail('presentation_schema_invalid');
|
|
70
|
+
}
|
|
71
|
+
if (planKeys.has(plan.plan_key)) fail('presentation_plan_duplicate', { plan_key: plan.plan_key });
|
|
72
|
+
planKeys.add(plan.plan_key);
|
|
73
|
+
const lanes = new Set();
|
|
74
|
+
for (const lane of plan.lanes) {
|
|
75
|
+
if (!exactRecord(lane, ['lane', 'name', 'description']) || !isTodoIdentifier(lane.lane)
|
|
76
|
+
|| !boundedText(lane.name, MAX_NAME_BYTES)
|
|
77
|
+
|| !boundedText(lane.description, MAX_DESCRIPTION_BYTES)) {
|
|
78
|
+
fail('presentation_schema_invalid');
|
|
79
|
+
}
|
|
80
|
+
if (lanes.has(lane.lane)) {
|
|
81
|
+
fail('presentation_lane_duplicate', { plan_key: plan.plan_key, lane: lane.lane });
|
|
82
|
+
}
|
|
83
|
+
lanes.add(lane.lane);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizedTaskNumber(taskId) {
|
|
90
|
+
const suffix = NUMERIC_SUFFIX.exec(taskId)?.[1] ?? null;
|
|
91
|
+
if (suffix === null || suffix.length > MAX_TASK_NUMBER_DIGITS) return null;
|
|
92
|
+
return { display_number: suffix, normalized_number: suffix.replace(/^0+(?=\d)/u, '') };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function taskNumberBindings(readModel) {
|
|
96
|
+
const candidatesByPlan = new Map();
|
|
97
|
+
for (const member of readModel.members) {
|
|
98
|
+
const plan = member?.plan;
|
|
99
|
+
for (const task of plan?.tasks ?? []) {
|
|
100
|
+
const candidate = normalizedTaskNumber(task.task_id);
|
|
101
|
+
if (candidate === null) continue;
|
|
102
|
+
const planKey = JSON.stringify([plan.project_id, plan.plan_key]);
|
|
103
|
+
if (!candidatesByPlan.has(planKey)) candidatesByPlan.set(planKey, new Map());
|
|
104
|
+
const byNumber = candidatesByPlan.get(planKey);
|
|
105
|
+
if (!byNumber.has(candidate.normalized_number)) byNumber.set(candidate.normalized_number, []);
|
|
106
|
+
byNumber.get(candidate.normalized_number).push({
|
|
107
|
+
project_id: plan.project_id,
|
|
108
|
+
plan_key: plan.plan_key,
|
|
109
|
+
task_id: task.task_id,
|
|
110
|
+
...candidate,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const bindings = [];
|
|
115
|
+
for (const byNumber of candidatesByPlan.values()) {
|
|
116
|
+
for (const candidates of byNumber.values()) if (candidates.length === 1) bindings.push(candidates[0]);
|
|
117
|
+
}
|
|
118
|
+
const globalCounts = new Map();
|
|
119
|
+
for (const binding of bindings) {
|
|
120
|
+
globalCounts.set(binding.normalized_number, (globalCounts.get(binding.normalized_number) ?? 0) + 1);
|
|
121
|
+
}
|
|
122
|
+
return bindings.map((binding) => ({
|
|
123
|
+
...binding,
|
|
124
|
+
globally_unique: globalCounts.get(binding.normalized_number) === 1,
|
|
125
|
+
})).sort((left, right) => compareText(left.project_id, right.project_id)
|
|
126
|
+
|| compareText(left.plan_key, right.plan_key)
|
|
127
|
+
|| compareText(left.task_id, right.task_id));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function projectTodoGanttPresentation(readModel, document) {
|
|
131
|
+
if (!exactRecord(readModel, ['schema', 'project_id', 'manifest', 'members', 'snapshot_stale'])
|
|
132
|
+
&& !(readModel?.schema === 'lattice.todo_store_read.v1'
|
|
133
|
+
&& isTodoIdentifier(readModel.project_id) && Array.isArray(readModel.members))) {
|
|
134
|
+
throw new TypeError('readModel must be lattice.todo_store_read.v1');
|
|
135
|
+
}
|
|
136
|
+
if (document !== null) validateDocument(document);
|
|
137
|
+
if (document !== null && document.project_id !== readModel.project_id) {
|
|
138
|
+
fail('presentation_project_mismatch', {
|
|
139
|
+
expected_project_id: readModel.project_id,
|
|
140
|
+
actual_project_id: document.project_id,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const planLanes = new Map();
|
|
145
|
+
for (const member of readModel.members) {
|
|
146
|
+
const plan = member?.plan;
|
|
147
|
+
if (!isTodoIdentifier(plan?.project_id) || !isTodoIdentifier(plan?.plan_key)
|
|
148
|
+
|| !Array.isArray(plan?.tasks)) throw new TypeError('readModel member plan is invalid');
|
|
149
|
+
planLanes.set(plan.plan_key, new Set(plan.tasks.map(({ lane }) => lane)));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const lanes = [];
|
|
153
|
+
for (const plan of document?.plans ?? []) {
|
|
154
|
+
const knownLanes = planLanes.get(plan.plan_key);
|
|
155
|
+
if (knownLanes === undefined) fail('presentation_plan_unknown', { plan_key: plan.plan_key });
|
|
156
|
+
for (const lane of plan.lanes) {
|
|
157
|
+
if (!knownLanes.has(lane.lane)) {
|
|
158
|
+
fail('presentation_lane_unknown', { plan_key: plan.plan_key, lane: lane.lane });
|
|
159
|
+
}
|
|
160
|
+
lanes.push({ plan_key: plan.plan_key, ...lane });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
lanes.sort((left, right) => compareText(left.plan_key, right.plan_key)
|
|
164
|
+
|| compareText(left.lane, right.lane));
|
|
165
|
+
|
|
166
|
+
const model = {
|
|
167
|
+
schema: TODO_GANTT_PRESENTATION_MODEL_SCHEMA,
|
|
168
|
+
project_id: readModel.project_id,
|
|
169
|
+
configured: document !== null,
|
|
170
|
+
lanes,
|
|
171
|
+
task_numbers: taskNumberBindings(readModel),
|
|
172
|
+
};
|
|
173
|
+
return { ...model, presentation_digest: digestTodoArtifact(model) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function readDocument(repoRoot) {
|
|
177
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
178
|
+
const absolute = path.resolve(canonicalRoot, TODO_GANTT_PRESENTATION_REF);
|
|
179
|
+
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) fail('presentation_path_outside_repo');
|
|
180
|
+
let stats;
|
|
181
|
+
try {
|
|
182
|
+
stats = await lstat(absolute);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error?.code === 'ENOENT') return null;
|
|
185
|
+
fail('presentation_unreadable');
|
|
186
|
+
}
|
|
187
|
+
if (stats.isSymbolicLink() || !stats.isFile() || stats.size > MAX_PRESENTATION_BYTES) {
|
|
188
|
+
fail(stats.size > MAX_PRESENTATION_BYTES
|
|
189
|
+
? 'presentation_size_limit_exceeded' : 'presentation_path_unsafe');
|
|
190
|
+
}
|
|
191
|
+
const resolved = await realpath(absolute);
|
|
192
|
+
if (resolved !== absolute || !within(canonicalRoot, resolved)) fail('presentation_path_unsafe');
|
|
193
|
+
const bytes = await readFile(resolved);
|
|
194
|
+
if (bytes.length > MAX_PRESENTATION_BYTES) fail('presentation_size_limit_exceeded');
|
|
195
|
+
let text;
|
|
196
|
+
try {
|
|
197
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
198
|
+
} catch {
|
|
199
|
+
fail('presentation_invalid_utf8');
|
|
200
|
+
}
|
|
201
|
+
const errors = [];
|
|
202
|
+
const tree = parseTree(text, errors, { allowTrailingComma: false, disallowComments: true });
|
|
203
|
+
if (errors.length > 0 || tree === undefined) fail('presentation_json_parse_failed');
|
|
204
|
+
if (hasDuplicateJsonKey(tree)) fail('presentation_duplicate_key');
|
|
205
|
+
let document;
|
|
206
|
+
try {
|
|
207
|
+
document = JSON.parse(text);
|
|
208
|
+
} catch {
|
|
209
|
+
fail('presentation_json_parse_failed');
|
|
210
|
+
}
|
|
211
|
+
return validateDocument(document);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function loadTodoGanttPresentation({ repoRoot, readModel }) {
|
|
215
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) throw new TypeError('repoRoot is required');
|
|
216
|
+
return projectTodoGanttPresentation(readModel, await readDocument(repoRoot));
|
|
217
|
+
}
|