elanous 0.2.18 → 0.2.20
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +16 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +16 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +24 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// GATE-PARTIAL (0.2.19 · 10-07): a re-gate after `release cut-branch --append` runs only the test files that can have
|
|
2
|
+
// changed — not the whole 24-shard sweep again (0.2.18: two timeout fixes on top of an 84-minute gate meant another full
|
|
3
|
+
// gate). The plan is opt-in (`release resume --from gate --partial`) and fail-closed: any non-test source change, a prior
|
|
4
|
+
// gate record for another commit, or a missing record means «full gate», never a guess.
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export interface PartialPlan {
|
|
10
|
+
version: string;
|
|
11
|
+
/** Commit the previous (full) gate measured — its recorded failures are carried for every file not re-run. */
|
|
12
|
+
priorCommit: string;
|
|
13
|
+
/** The commit this plan is for — a gate on any other commit ignores it. */
|
|
14
|
+
forCommit: string;
|
|
15
|
+
/** Test files re-run: changed test files ⊕ files of the prior failures. */
|
|
16
|
+
files: string[];
|
|
17
|
+
priorFailures: string[];
|
|
18
|
+
priorErrors: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const partialPlanPath = (root: string, version: string) => join(root, 'release', version, 'gate-partial.json');
|
|
22
|
+
/** The same test-file set the gate sweep runs (`git ls-files` filtered by this suffix). */
|
|
23
|
+
const TEST_FILE = /^(?:[\w.-]+\/)*[\w.-]+\.test\.(?:tsx?|jsx?|mts|cts)$/;
|
|
24
|
+
/** Changes that cannot alter a test result on their own: Markdown that is not test input. A fixture (any file under a
|
|
25
|
+
* test/, fixtures, __fixtures__ or __snapshots__ path), JSON or script is not inert — the gate runs in full. */
|
|
26
|
+
const INERT_MD = /\.md$/;
|
|
27
|
+
const TEST_INPUT = /(?:^|\/)(?:test|tests|fixtures|__fixtures__|__snapshots__|__mocks__)\//;
|
|
28
|
+
const isInert = (file: string) => INERT_MD.test(file) && !TEST_INPUT.test(file);
|
|
29
|
+
|
|
30
|
+
/** The test file of a failure id (`<file> > <case>`). One rule for the gate and the partial plan: ids come only from the
|
|
31
|
+
* gate's output parsers, which accept `dir/….test.ts(x)` — anything else (a root file, `..`, no case) is unsafe. */
|
|
32
|
+
export function testFileOfId(id: string): string {
|
|
33
|
+
const file = id.split(' > ', 1)[0]!;
|
|
34
|
+
if (!id.includes(' > ') || !/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Decide whether a partial re-gate is safe for `changedFiles` (prior → for). Never guesses: any doubt is a full gate. */
|
|
39
|
+
export function planPartialRegate(input: { version: string; priorCommit: string; forCommit: string; changedFiles: readonly string[];
|
|
40
|
+
priorFailures: readonly string[]; priorErrors: readonly string[] }):
|
|
41
|
+
{ ok: true; plan: PartialPlan } | { ok: false; reason: string } {
|
|
42
|
+
if (!/^[0-9a-f]{40}$/i.test(input.priorCommit) || !/^[0-9a-f]{40}$/i.test(input.forCommit)) return { ok: false, reason: 'commit is not a full sha' };
|
|
43
|
+
if (input.priorCommit === input.forCommit) return { ok: false, reason: 'no new commit since the prior gate' };
|
|
44
|
+
const source = input.changedFiles.filter((file) => !TEST_FILE.test(file) && !isInert(file));
|
|
45
|
+
if (source.length) return { ok: false, reason: `non-test change → full gate: ${source.slice(0, 5).join(', ')}${source.length > 5 ? ` (+${source.length - 5})` : ''}` };
|
|
46
|
+
let failureFiles: string[];
|
|
47
|
+
try { failureFiles = [...input.priorFailures, ...input.priorErrors].map(testFileOfId); }
|
|
48
|
+
catch (error) { return { ok: false, reason: String(error instanceof Error ? error.message : error) }; }
|
|
49
|
+
const files = [...new Set([...input.changedFiles.filter((file) => TEST_FILE.test(file)), ...failureFiles])].sort();
|
|
50
|
+
if (!files.length) return { ok: false, reason: 'nothing to re-run' };
|
|
51
|
+
return { ok: true, plan: { version: input.version, priorCommit: input.priorCommit, forCommit: input.forCommit, files,
|
|
52
|
+
priorFailures: [...input.priorFailures], priorErrors: [...input.priorErrors] } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writePartialPlan(root: string, plan: PartialPlan): string {
|
|
56
|
+
const path = partialPlanPath(root, plan.version);
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
59
|
+
writeFileSync(temp, `${JSON.stringify(plan, null, 2)}\n`, { mode: 0o600 });
|
|
60
|
+
renameSync(temp, path);
|
|
61
|
+
return path;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clearPartialPlan(root: string, version: string): void {
|
|
65
|
+
const path = partialPlanPath(root, version);
|
|
66
|
+
if (existsSync(path)) rmSync(path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The plan for exactly this commit, or null. A malformed plan is an error (never silently a full or partial gate). */
|
|
70
|
+
export function readPartialPlan(roots: readonly string[], version: string, commit: string): PartialPlan | null {
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
const path = partialPlanPath(root, version);
|
|
73
|
+
if (!existsSync(path)) continue;
|
|
74
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as Partial<PartialPlan>;
|
|
75
|
+
if (raw.version !== version || typeof raw.priorCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(raw.priorCommit)
|
|
76
|
+
|| typeof raw.forCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(raw.forCommit) || raw.priorCommit === raw.forCommit
|
|
77
|
+
|| !Array.isArray(raw.files) || raw.files.length === 0 || !raw.files.every((file) => typeof file === 'string' && TEST_FILE.test(file) && !file.split('/').includes('..'))
|
|
78
|
+
|| !Array.isArray(raw.priorFailures) || !Array.isArray(raw.priorErrors)) throw new Error(`invalid gate partial plan: ${path}`);
|
|
79
|
+
// Every prior failure's file is re-run (planPartialRegate guarantees it) — a plan that would carry a failure it did
|
|
80
|
+
// not re-run is malformed, not «partial».
|
|
81
|
+
const rerun = new Set(raw.files);
|
|
82
|
+
let failureFiles: string[];
|
|
83
|
+
try { failureFiles = [...raw.priorFailures, ...raw.priorErrors].map((id) => testFileOfId(String(id))); }
|
|
84
|
+
catch { throw new Error(`invalid gate partial plan: ${path}`); }
|
|
85
|
+
if (failureFiles.some((file) => !rerun.has(file))) throw new Error(`invalid gate partial plan (prior failure not re-run): ${path}`);
|
|
86
|
+
if (raw.forCommit !== commit) continue;
|
|
87
|
+
return raw as PartialPlan;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Cut failures after a partial re-run: prior results for files not re-run ⊕ this run's results for the re-run files. */
|
|
93
|
+
export function mergePartialFailures(plan: PartialPlan, measured: { failures: string[]; errors: string[] }): { failures: string[]; errors: string[] } {
|
|
94
|
+
const rerun = new Set(plan.files);
|
|
95
|
+
const keep = (ids: readonly string[]) => ids.filter((id) => !rerun.has(testFileOfId(id)));
|
|
96
|
+
return { failures: [...keep(plan.priorFailures), ...measured.failures], errors: [...keep(plan.priorErrors), ...measured.errors] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Files changed between two commits — both sides of a rename (`--no-renames`), so moving a source file into a test or
|
|
100
|
+
* Markdown name still shows the source path that disappeared. */
|
|
101
|
+
export function changedFilesBetween(repo: string, from: string, to: string): string[] {
|
|
102
|
+
// git-spawn-allow: read-only diff of two release commits.
|
|
103
|
+
const result = spawnSync('git', ['diff', '--name-only', '--no-renames', `${from}..${to}`], { cwd: repo, encoding: 'utf8' });
|
|
104
|
+
if (result.status !== 0 || result.error) throw new Error(`git diff failed: ${(result.stderr || result.error || 'no output').toString().trim()}`);
|
|
105
|
+
return result.stdout.split(/\r?\n/).filter(Boolean);
|
|
106
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// F5 GATE-TIMING-REPORT — 게이트 한 판의 «시간 비용»을 보이게 한다(GATE-SPEED 의 자).
|
|
2
|
+
// 입력 = 게이트 로그 디렉토리(`<ledger>/release/<version>/gate-logs/cut`)의 `pod-*.json`.
|
|
3
|
+
// 첫 판 샤드 = `pod-<n>.json` · 재시도 = 그 밖의 모든 `pod-*.json`(`-c<k>`·`-c<k>-file-<m>`·`-retry` …).
|
|
4
|
+
// ⛔ 측정이 없으면 `measured:false` 로 표시한다 — 0 을 «잰 값»처럼 내지 않는다.
|
|
5
|
+
// ⭐ 기본 = 전 우주(LOGS-DEFAULT-ALL · 0.2.20) — 게이트는 실행 트리의 시험 우주(예: ⟨test:wt-release⟩
|
|
6
|
+
// `<tree>/.elanous-test/release/<v>/gate-logs/cut`)에 쌓인다. 운영 원장만 보면 0.2.19 판처럼
|
|
7
|
+
// «측정 없음»으로 오판한다. 그래서 버전 인자는 운영 원장 ⊕ 등록된 모든 우주(시험·파생 시험 포함)를
|
|
8
|
+
// 본다 — 우주 열거는 `eln logs --all --include-test` 와 같은 레지스트리(`readLogInstanceScope`)다.
|
|
9
|
+
// 좁히기는 opt-in `--prod-only`(종전 동작: `releaseLedgerRoot()` 하나).
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
13
|
+
import { readLogInstanceScope } from '../../src/mss/logging/instance-registry.js';
|
|
14
|
+
|
|
15
|
+
export interface ShardTiming { name: string; min: number; files: number; plannedMin: number | null }
|
|
16
|
+
|
|
17
|
+
export interface GateTimingReport {
|
|
18
|
+
dir: string;
|
|
19
|
+
/** 읽은 pod-*.json 중 durationMs 를 가진 것이 하나라도 있었나. false 면 아래 수는 «측정 없음». */
|
|
20
|
+
measured: boolean;
|
|
21
|
+
/** JSON 파싱 실패·durationMs 부재로 건너뛴 파일 이름. */
|
|
22
|
+
unreadable: string[];
|
|
23
|
+
firstPass: { shards: number; sumMin: number; medianMin: number; maxMin: number; failedShards: string[] };
|
|
24
|
+
retries: { runs: number; sumMin: number; medianMin: number };
|
|
25
|
+
/** 재시도 합 / (첫 판 합 + 재시도 합). 측정이 없으면 null. */
|
|
26
|
+
retryShare: number | null;
|
|
27
|
+
slowestShards: ShardTiming[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FIRST_PASS = /^pod-\d+\.json$/;
|
|
31
|
+
const POD_JSON = /^pod-.+\.json$/;
|
|
32
|
+
|
|
33
|
+
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
34
|
+
|
|
35
|
+
function median(xs: number[]): number {
|
|
36
|
+
if (xs.length === 0) return 0;
|
|
37
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
38
|
+
const m = s.length >> 1;
|
|
39
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PodRecord { durationMs?: unknown; rc?: unknown; files?: unknown; plannedSeconds?: unknown; jobFailed?: unknown }
|
|
43
|
+
|
|
44
|
+
export function gateTiming(dir: string): GateTimingReport {
|
|
45
|
+
const names = existsSync(dir) && statSync(dir).isDirectory()
|
|
46
|
+
? readdirSync(dir).filter((n) => POD_JSON.test(n)).sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
|
|
47
|
+
: [];
|
|
48
|
+
const first: Array<ShardTiming & { failed: boolean }> = [];
|
|
49
|
+
const retryMin: number[] = [];
|
|
50
|
+
const unreadable: string[] = [];
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
let rec: PodRecord;
|
|
53
|
+
try { rec = JSON.parse(readFileSync(join(dir, name), 'utf8')) as PodRecord; } catch { unreadable.push(name); continue; }
|
|
54
|
+
if (!rec || typeof rec !== 'object' || typeof rec.durationMs !== 'number' || !Number.isFinite(rec.durationMs)) { unreadable.push(name); continue; }
|
|
55
|
+
const min = rec.durationMs / 60_000;
|
|
56
|
+
if (FIRST_PASS.test(name)) {
|
|
57
|
+
first.push({
|
|
58
|
+
name: name.replace(/\.json$/, ''),
|
|
59
|
+
min,
|
|
60
|
+
files: Array.isArray(rec.files) ? rec.files.length : 0,
|
|
61
|
+
plannedMin: typeof rec.plannedSeconds === 'number' ? rec.plannedSeconds / 60 : null,
|
|
62
|
+
failed: rec.jobFailed === true || (typeof rec.rc === 'number' && rec.rc !== 0),
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
retryMin.push(min);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const firstMins = first.map((s) => s.min);
|
|
69
|
+
const firstSum = firstMins.reduce((a, b) => a + b, 0);
|
|
70
|
+
const retrySum = retryMin.reduce((a, b) => a + b, 0);
|
|
71
|
+
const total = firstSum + retrySum;
|
|
72
|
+
const measured = first.length + retryMin.length > 0;
|
|
73
|
+
return {
|
|
74
|
+
dir,
|
|
75
|
+
measured,
|
|
76
|
+
unreadable,
|
|
77
|
+
firstPass: {
|
|
78
|
+
shards: first.length,
|
|
79
|
+
sumMin: round1(firstSum),
|
|
80
|
+
medianMin: round1(median(firstMins)),
|
|
81
|
+
maxMin: round1(firstMins.length ? Math.max(...firstMins) : 0),
|
|
82
|
+
failedShards: first.filter((s) => s.failed).map((s) => s.name),
|
|
83
|
+
},
|
|
84
|
+
retries: { runs: retryMin.length, sumMin: round1(retrySum), medianMin: round1(median(retryMin)) },
|
|
85
|
+
retryShare: measured && total > 0 ? Math.round((retrySum / total) * 1000) / 1000 : null,
|
|
86
|
+
slowestShards: [...first].sort((a, b) => b.min - a.min).slice(0, 5)
|
|
87
|
+
.map(({ name, min, files, plannedMin }) => ({ name, min: round1(min), files, plannedMin: plannedMin === null ? null : round1(plannedMin) })),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 인자가 버전이 아니라 디렉토리인가 — 경로처럼 보이거나 실제로 있으면. */
|
|
92
|
+
export function isGateLogDirArg(arg: string): boolean {
|
|
93
|
+
return arg.includes('/') || arg.startsWith('.') || existsSync(arg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `<version>` 이면 원장 루트(`releaseLedgerRoot()`)의 `release/<version>/gate-logs/cut`, 경로처럼 보이면 그대로. */
|
|
97
|
+
export function resolveGateLogDir(arg: string, ledgerRoot: string = releaseLedgerRoot()): string {
|
|
98
|
+
if (isGateLogDirArg(arg)) return arg;
|
|
99
|
+
return join(ledgerRoot, 'release', arg, 'gate-logs', 'cut');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface GateLogCandidate { universe: string; dir: string }
|
|
103
|
+
|
|
104
|
+
export interface GateLogScope {
|
|
105
|
+
/** 본 후보 원장 루트 수(물리 경로 dedup 뒤). */
|
|
106
|
+
roots: number;
|
|
107
|
+
/** 우주 레지스트리 — `read` 읽음 · `unreadable` 못 읽음(«등록 우주 0»이 아니라 «못 셈» — 운영 원장만 봤다)
|
|
108
|
+
* · `skipped` `--prod-only` 라 안 읽음. */
|
|
109
|
+
registry: 'read' | 'unreadable' | 'skipped';
|
|
110
|
+
/** `--prod-only` 로 좁혔나. */
|
|
111
|
+
prodOnly: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface GateLogScopeDeps {
|
|
115
|
+
ledgerRoot?: string;
|
|
116
|
+
readInstances?: () => { instances: ReadonlyArray<{ name: string; stateDir: string }>; queryStatus: { registeredStores: boolean } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function physical(path: string): string {
|
|
120
|
+
try { return realpathSync(path); } catch { return path; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 버전의 게이트 로그 후보 — 운영 원장 루트(`releaseLedgerRoot()`) ⊕ 등록된 모든 우주.
|
|
124
|
+
* 물리 경로로 dedup 한다(`~/.monad` → `~/.elanous` 심링크가 같은 판을 두 번 세지 않게). */
|
|
125
|
+
export function gateLogCandidates(
|
|
126
|
+
version: string,
|
|
127
|
+
opts: { prodOnly?: boolean } = {},
|
|
128
|
+
deps: GateLogScopeDeps = {},
|
|
129
|
+
): { candidates: GateLogCandidate[]; scope: GateLogScope } {
|
|
130
|
+
const primary = deps.ledgerRoot ?? releaseLedgerRoot();
|
|
131
|
+
const roots: Array<{ universe: string; root: string }> = [{ universe: 'prod', root: primary }];
|
|
132
|
+
let registry: GateLogScope['registry'] = 'skipped';
|
|
133
|
+
if (!opts.prodOnly) {
|
|
134
|
+
const scope = (deps.readInstances ?? readLogInstanceScope)();
|
|
135
|
+
registry = scope.queryStatus.registeredStores ? 'read' : 'unreadable';
|
|
136
|
+
// 못 읽은 판의 부분 목록은 쓰지 않는다 — «운영만 봤다»는 표시와 실제 본 범위를 일치시킨다.
|
|
137
|
+
if (registry === 'read') for (const instance of scope.instances) roots.push({ universe: instance.name, root: instance.stateDir });
|
|
138
|
+
}
|
|
139
|
+
const seen = new Set<string>();
|
|
140
|
+
const candidates: GateLogCandidate[] = [];
|
|
141
|
+
for (const { universe, root } of roots) {
|
|
142
|
+
const key = physical(root);
|
|
143
|
+
if (seen.has(key)) continue;
|
|
144
|
+
seen.add(key);
|
|
145
|
+
candidates.push({ universe, dir: resolveGateLogDir(version, root) });
|
|
146
|
+
}
|
|
147
|
+
return { candidates, scope: { roots: candidates.length, registry, prodOnly: opts.prodOnly === true } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface GateTimingScan {
|
|
151
|
+
version: string;
|
|
152
|
+
scope: GateLogScope & { found: number };
|
|
153
|
+
/** pod-*.json 디렉토리가 «있는» 우주만 — 판마다 따로 낸다(서로 다른 판의 샤드를 섞지 않는다). */
|
|
154
|
+
reports: Array<GateTimingReport & { universe: string }>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 버전 인자의 기본 동선 — 후보 우주 전부에서 게이트 로그 디렉토리가 있는 것만 보고한다. */
|
|
158
|
+
export function scanGateTiming(version: string, opts: { prodOnly?: boolean } = {}, deps: GateLogScopeDeps = {}): GateTimingScan {
|
|
159
|
+
const { candidates, scope } = gateLogCandidates(version, opts, deps);
|
|
160
|
+
const reports = candidates
|
|
161
|
+
.filter(({ dir }) => existsSync(dir) && statSync(dir).isDirectory())
|
|
162
|
+
.map(({ universe, dir }) => ({ universe, ...gateTiming(dir) }));
|
|
163
|
+
return { version, scope: { ...scope, found: reports.length }, reports };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function formatGateTimingScan(scan: GateTimingScan): string {
|
|
167
|
+
const s = scan.scope;
|
|
168
|
+
const head = `게이트 로그 ${scan.version} — ${s.prodOnly ? '운영 원장만(--prod-only)' : '전 우주'} · 본 원장 루트 ${s.roots}개 · 로그 있는 우주 ${s.found}개`
|
|
169
|
+
+ (s.registry === 'unreadable' ? ' · ⚠ 우주 레지스트리를 못 읽었다(운영만 봤다 — «없다»가 아니라 «못 셈»)' : '');
|
|
170
|
+
if (scan.reports.length === 0) {
|
|
171
|
+
const where = s.registry === 'read' ? '본 우주 어디에도' : '운영 원장에';
|
|
172
|
+
const unknown = s.registry === 'unreadable' ? ' · 시험 우주는 못 셌다(있을 수 있다)' : '';
|
|
173
|
+
return [head, ` 게이트 시간 — 측정 없음 · ${where} release/${scan.version}/gate-logs/cut 가 없다${unknown}`,
|
|
174
|
+
...(s.prodOnly ? [' (--prod-only 를 빼면 시험 우주까지 본다)'] : [])].join('\n');
|
|
175
|
+
}
|
|
176
|
+
return [head, ...scan.reports.map((r) => `⟨${r.universe}⟩ ${formatGateTiming(r)}`)].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function formatGateTiming(r: GateTimingReport): string {
|
|
180
|
+
if (!r.measured) {
|
|
181
|
+
return [`게이트 시간 — 측정 없음 (${r.dir})`, ' pod-*.json 에 durationMs 가 하나도 없다 · 0 은 «잰 값»이 아니다',
|
|
182
|
+
...(r.unreadable.length ? [` 읽지 못한 파일 ${r.unreadable.length}개`] : [])].join('\n');
|
|
183
|
+
}
|
|
184
|
+
const f = r.firstPass;
|
|
185
|
+
const lines = [
|
|
186
|
+
`게이트 시간 (${r.dir})`,
|
|
187
|
+
` 첫 판 샤드 ${f.shards}개 · 합 ${f.sumMin}분 · 중앙 ${f.medianMin}분 · 최대 ${f.maxMin}분 · 실패 샤드 ${f.failedShards.length}개${f.failedShards.length ? ` (${f.failedShards.join(', ')})` : ''}`,
|
|
188
|
+
` 재시도 ${r.retries.runs}회 · 합 ${r.retries.sumMin}분 · 중앙 ${r.retries.medianMin}분`,
|
|
189
|
+
` 재시도 비중 ${r.retryShare === null ? '측정 없음' : `${round1(r.retryShare * 100)}%`} (재시도 합 / 전체 합)`,
|
|
190
|
+
' 느린 샤드 상위 5:',
|
|
191
|
+
...r.slowestShards.map((s) => ` ${s.name} ${s.min}분 · 파일 ${s.files}개 · 계획 ${s.plannedMin === null ? '—' : `${s.plannedMin}분`}`),
|
|
192
|
+
];
|
|
193
|
+
if (r.unreadable.length) lines.push(` ⚠ 읽지 못한 파일 ${r.unreadable.length}개: ${r.unreadable.slice(0, 5).join(', ')}${r.unreadable.length > 5 ? ' …' : ''}`);
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const GATE_TIMING_USAGE = [
|
|
198
|
+
'기본 = 전 우주 — 운영 원장 ⊕ 시험 우주(⟨test:wt-release⟩ 등)의 게이트 로그를 옵션 없이 함께 읽는다',
|
|
199
|
+
'사용법: bun scripts/release-loop/gate-timing.ts <version|dir> [--json] [--prod-only]',
|
|
200
|
+
' <version> 등록된 모든 우주의 release/<version>/gate-logs/cut 를 우주마다 따로 보고한다(⟨우주⟩ 태그)',
|
|
201
|
+
' <dir> 그 디렉토리 하나만 (종전과 같은 단일 보고)',
|
|
202
|
+
' --prod-only 운영 원장 루트(releaseLedgerRoot)만 본다 — 종전 기본',
|
|
203
|
+
' --json 구조화 출력 (<version> 이면 { version, scope, reports[] })',
|
|
204
|
+
].join('\n');
|
|
205
|
+
|
|
206
|
+
const KNOWN_FLAGS = new Set(['--json', '--prod-only', '--help', '-h']);
|
|
207
|
+
|
|
208
|
+
if (import.meta.main) {
|
|
209
|
+
const args = process.argv.slice(2);
|
|
210
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(GATE_TIMING_USAGE); process.exit(0); }
|
|
211
|
+
const unknown = args.filter((a) => a.startsWith('-') && !KNOWN_FLAGS.has(a));
|
|
212
|
+
if (unknown.length) { console.error(`알 수 없는 옵션: ${unknown.join(' ')}\n${GATE_TIMING_USAGE}`); process.exit(2); }
|
|
213
|
+
const json = args.includes('--json');
|
|
214
|
+
const prodOnly = args.includes('--prod-only');
|
|
215
|
+
const target = args.find((a) => !a.startsWith('-'));
|
|
216
|
+
if (!target) {
|
|
217
|
+
console.error(GATE_TIMING_USAGE);
|
|
218
|
+
process.exit(2);
|
|
219
|
+
}
|
|
220
|
+
if (isGateLogDirArg(target)) {
|
|
221
|
+
const report = gateTiming(target);
|
|
222
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatGateTiming(report));
|
|
223
|
+
if (!report.measured) process.exit(1);
|
|
224
|
+
} else {
|
|
225
|
+
const scan = scanGateTiming(target, { prodOnly });
|
|
226
|
+
console.log(json ? JSON.stringify(scan, null, 2) : formatGateTimingScan(scan));
|
|
227
|
+
if (!scan.reports.some((r) => r.measured)) process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -27,7 +27,7 @@ export interface PreflightResult {
|
|
|
27
27
|
blockers: number;
|
|
28
28
|
warnings: number;
|
|
29
29
|
findings: PreflightFinding[];
|
|
30
|
-
schedule: { cutAt: string; landBy: string | null; publishAt: string | null; publishAtSource: 'option' | 'cadence' | null } | null;
|
|
30
|
+
schedule: { cutAt: string; landBy: string | null; publishAt: string | null; publishAtSource: 'option' | 'schedule' | 'cadence' | null } | null;
|
|
31
31
|
/** The immediately previous release (by version order) whose gate is compared. */
|
|
32
32
|
previousVersion: string | null;
|
|
33
33
|
gateMeasure: GateMeasure | null;
|
|
@@ -206,15 +206,17 @@ export async function releasePreflight(version: string, deps: PreflightDeps = {}
|
|
|
206
206
|
findings.push({ level: 'warn', check: 'schedule', message: `판 일정 없음 — elanous release schedule set --version ${version} --cut-at <iso> --land-by <iso>` });
|
|
207
207
|
} else {
|
|
208
208
|
const cut = Date.parse(schedule.cutAt);
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
// --publish-at > the stored schedule publish time (`release schedule set --publish-at`) > the October cadence.
|
|
210
|
+
const publishAtSource = deps.publishAt ? 'option' : schedule.publishAt ? 'schedule' : 'cadence';
|
|
211
|
+
const publishAt = deps.publishAt ? new Date(deps.publishAt).toISOString() : schedule.publishAt ?? cadencePublishAt(schedule.cutAt);
|
|
212
|
+
scheduleOut = { cutAt: schedule.cutAt, landBy: schedule.landBy, publishAt, publishAtSource };
|
|
211
213
|
if (!schedule.landBy) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 없음 — 컷 ${formatKst(schedule.cutAt)} · 규칙은 컷 − 30분` });
|
|
212
214
|
else if (cut - Date.parse(schedule.landBy) < 30 * 60_000) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 ${formatKst(schedule.landBy)} 이 컷 − 30분보다 늦다(컷 ${formatKst(schedule.cutAt)})` });
|
|
213
215
|
else findings.push({ level: 'ok', check: 'schedule', message: `일정 — 착지 마감 ${formatKst(schedule.landBy)} · 컷 ${formatKst(schedule.cutAt)}` });
|
|
214
216
|
gateMeasure = previousVersion ? releaseGateMeasure({ version: previousVersion }, deps.graphRunsRoot) : null;
|
|
215
217
|
if (!gateMeasure) fallbackGateMeasure = releaseGateMeasure({ below: version }, deps.graphRunsRoot);
|
|
216
218
|
const windowSeconds = Math.round((Date.parse(publishAt) - cut) / 1000);
|
|
217
|
-
const publishLabel = `발행 ${formatKst(publishAt)}${
|
|
219
|
+
const publishLabel = `발행 ${formatKst(publishAt)}${publishAtSource === 'cadence' ? '(기본: 판 주기 07·18시 · --publish-at 로 바꾼다)' : publishAtSource === 'schedule' ? '(판 일정)' : ''}`;
|
|
218
220
|
if (!gateMeasure) {
|
|
219
221
|
const fallback = fallbackGateMeasure ? ` · 참고: 가장 최근 실측은 ${fallbackGateMeasure.version} 게이트 ${minutes(fallbackGateMeasure.gateSeconds)}(직전 판 아님)` : ' · 게이트 실측 없음';
|
|
220
222
|
findings.push({ level: 'warn', check: 'schedule', message: `직전 판${previousVersion ? `(${previousVersion})` : ''} 실측 없음 — 컷→${publishLabel} 창 ${minutes(windowSeconds)}${fallback}` });
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// PUBLISH-TIME-FROM-GATE (0.2.20 P1) — 발행 시각을 사람이 고르지 않고 «실측»에서 제안한다.
|
|
2
|
+
//
|
|
3
|
+
// 🩸 계기: 판 일정이 «컷 18:45 → 발행 20:00(75분)»처럼 잡혔는데 게이트 하나가 60분+ · 게이트 뒤 노드가 ~40분이다.
|
|
4
|
+
// ⭐ 자 = 최근 N(기본 3)개 완료된(`status: done` · 안정 버전) release-loop 런마다 «런 시작(≈ 컷) → 발행 노드 끝»을 재고 중앙값을 쓴다.
|
|
5
|
+
// 창은 «최근 N개 완료 런»으로 고정한다 — 못 잰 런 대신 더 옛 런을 끌어오지 않는다(옛 판이 지금 실측인 척 섞이지 않게).
|
|
6
|
+
// 런 시작 = 컷이다(예약 시작이 컷에 런을 띄운다) — preflight 의 «런 시작→발행 노드»와 같은 기점.
|
|
7
|
+
// ⛔ 못 잰 런은 0 으로 세지 않는다 — 건너뛰고 `skipped` 에 이유와 함께 남긴다. 표본이 0 이면 제안도 경고도 «없음»이다.
|
|
8
|
+
//
|
|
9
|
+
// 시각 출처(우선순위):
|
|
10
|
+
// ① `nodes[]` 의 `startedAt`·`endedAt`(0.2.19~ 원장이 노드 시각을 적는다)
|
|
11
|
+
// ② 옛 원장 — `<runId>.json.contexts/<n>.json` 의 mtime(노드 n 이 «시작할 때» 쓰인다) ⇒ 발행 노드 끝 ≈ 다음 노드 시작.
|
|
12
|
+
// (preflight `releaseGateMeasure` 와 같은 근사 · 그 파일의 nodeId 가 path 와 맞을 때만 쓴다)
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_PUBLISH_SAMPLE = 3;
|
|
18
|
+
/** 중앙값 위에 얹는 여유(분). 제안은 그 뒤 5분 단위로 올린다. */
|
|
19
|
+
export const DEFAULT_PUBLISH_MARGIN_MIN = 10;
|
|
20
|
+
|
|
21
|
+
const STABLE = /^\d+\.\d+\.\d+$/;
|
|
22
|
+
|
|
23
|
+
export interface PublishDurationSample {
|
|
24
|
+
version: string;
|
|
25
|
+
runId: string;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
/** 런 시작 → 발행 노드 끝(초). */
|
|
28
|
+
seconds: number;
|
|
29
|
+
source: 'nodes' | 'contexts';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PublishWindowMeasure {
|
|
33
|
+
root: string;
|
|
34
|
+
/** 중앙값에 쓴 표본(최신 먼저 · 최근 sample 개 완료 런 중 잰 것). */
|
|
35
|
+
samples: PublishDurationSample[];
|
|
36
|
+
/** 창 안의 완료 런이었지만 못 잰 것 — «0»이 아니라 «못 셈». */
|
|
37
|
+
skipped: Array<{ runId: string; version: string; why: string }>;
|
|
38
|
+
/** 표본이 없으면 null(측정 없음). */
|
|
39
|
+
medianSeconds: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RunState {
|
|
43
|
+
runId?: string;
|
|
44
|
+
status?: string;
|
|
45
|
+
startedAt?: string;
|
|
46
|
+
path?: unknown;
|
|
47
|
+
input?: { version?: string };
|
|
48
|
+
nodes?: Array<{ nodeId?: string; startedAt?: string; endedAt?: string }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function defaultReleaseGraphRunsRoot(ledgerRoot: string = releaseLedgerRoot()): string {
|
|
52
|
+
return join(ledgerRoot, 'graph-runs', 'release-loop');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function median(xs: number[]): number {
|
|
56
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
57
|
+
const m = s.length >> 1;
|
|
58
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function publishEndMs(root: string, file: string, state: RunState, path: string[]): { ms: number; source: PublishDurationSample['source'] } | null {
|
|
62
|
+
const node = Array.isArray(state.nodes) ? state.nodes.find((n) => n?.nodeId === 'publish') : undefined;
|
|
63
|
+
const ended = node?.endedAt ? Date.parse(node.endedAt) : NaN;
|
|
64
|
+
if (Number.isFinite(ended)) return { ms: ended, source: 'nodes' };
|
|
65
|
+
const index = path.indexOf('publish');
|
|
66
|
+
const contexts = join(root, `${file}.contexts`);
|
|
67
|
+
if (index < 0 || index + 1 >= path.length || !existsSync(contexts)) return null;
|
|
68
|
+
const ctxPath = join(contexts, `${index + 2}.json`); // contexts/<n>.json 은 1부터 — 발행 다음 노드의 시작
|
|
69
|
+
try {
|
|
70
|
+
const ctx = JSON.parse(readFileSync(ctxPath, 'utf8')) as { nodeId?: string };
|
|
71
|
+
return ctx.nodeId === path[index + 1] ? { ms: statSync(ctxPath).mtimeMs, source: 'contexts' } : null;
|
|
72
|
+
} catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 최근 `sample` 개 완료 release-loop 런의 «런 시작 → 발행 노드 끝» 중앙값. */
|
|
76
|
+
export function measurePublishWindow(opts: { root?: string; sample?: number } = {}): PublishWindowMeasure {
|
|
77
|
+
const root = opts.root ?? defaultReleaseGraphRunsRoot();
|
|
78
|
+
const sample = Math.max(1, opts.sample ?? DEFAULT_PUBLISH_SAMPLE);
|
|
79
|
+
const runs: Array<{ file: string; state: RunState; version: string; startedAt: string; startMs: number; path: string[] | null }> = [];
|
|
80
|
+
if (existsSync(root)) {
|
|
81
|
+
for (const file of readdirSync(root)) {
|
|
82
|
+
if (!file.endsWith('.json') || file.includes('.decision')) continue;
|
|
83
|
+
try {
|
|
84
|
+
const state = JSON.parse(readFileSync(join(root, file), 'utf8')) as RunState;
|
|
85
|
+
const v = state.input?.version;
|
|
86
|
+
if (state.status !== 'done' || typeof v !== 'string' || !STABLE.test(v)) continue;
|
|
87
|
+
// A completed stable run with a broken start or path is still «a run in the window» — it lands in `skipped`, not nowhere.
|
|
88
|
+
const startedAt = typeof state.startedAt === 'string' ? state.startedAt : '';
|
|
89
|
+
runs.push({ file, state, version: v, startedAt, startMs: Date.parse(startedAt), path: Array.isArray(state.path) ? state.path.filter((p): p is string => typeof p === 'string') : null });
|
|
90
|
+
} catch { /* 못 읽은 원장은 표본이 아니다 */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Newest first; a run without a readable start sorts by its file mtime so it still takes its place in the window.
|
|
94
|
+
const order = (run: (typeof runs)[number]) => (Number.isFinite(run.startMs) ? run.startMs : statSync(join(root, run.file)).mtimeMs);
|
|
95
|
+
runs.sort((a, b) => order(b) - order(a));
|
|
96
|
+
const samples: PublishDurationSample[] = [];
|
|
97
|
+
const skipped: PublishWindowMeasure['skipped'] = [];
|
|
98
|
+
for (const run of runs.slice(0, sample)) {
|
|
99
|
+
const runId = run.state.runId ?? run.file.slice(0, -5);
|
|
100
|
+
if (!Number.isFinite(run.startMs)) { skipped.push({ runId, version: run.version, why: 'startedAt 없음·무효' }); continue; }
|
|
101
|
+
if (!run.path) { skipped.push({ runId, version: run.version, why: 'path 가 배열이 아님' }); continue; }
|
|
102
|
+
if (!run.path.includes('publish')) { skipped.push({ runId, version: run.version, why: 'path 에 publish 노드 없음' }); continue; }
|
|
103
|
+
const end = publishEndMs(root, run.file, run.state, run.path);
|
|
104
|
+
const start = run.startMs;
|
|
105
|
+
if (!end || end.ms < start) { skipped.push({ runId, version: run.version, why: '발행 노드 끝 시각을 못 읽음' }); continue; }
|
|
106
|
+
samples.push({ version: run.version, runId, startedAt: run.startedAt, seconds: Math.round((end.ms - start) / 1000), source: end.source });
|
|
107
|
+
}
|
|
108
|
+
return { root, samples, skipped, medianSeconds: samples.length ? Math.round(median(samples.map((s) => s.seconds))) : null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 제안 발행 시각 = 컷 + 중앙값 + 여유, 5분 단위로 올림. 측정이 없으면 null. */
|
|
112
|
+
export function proposePublishAt(cutAt: string, measure: PublishWindowMeasure, marginMin = DEFAULT_PUBLISH_MARGIN_MIN): string | null {
|
|
113
|
+
if (measure.medianSeconds === null) return null;
|
|
114
|
+
const raw = Date.parse(cutAt) + measure.medianSeconds * 1000 + marginMin * 60_000;
|
|
115
|
+
const step = 5 * 60_000;
|
|
116
|
+
return new Date(Math.ceil(raw / step) * step).toISOString();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function kstHourMinute(iso: string): string {
|
|
120
|
+
return new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(new Date(iso));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mins = (seconds: number) => Math.round(seconds / 60);
|
|
124
|
+
|
|
125
|
+
/** 발행 시각이 실측 중앙값보다 짧으면 한 줄 경고, 아니면 null(측정 없음도 null — 경고할 자가 없다). */
|
|
126
|
+
export function publishWindowWarning(cutAt: string, publishAt: string, measure: PublishWindowMeasure, marginMin = DEFAULT_PUBLISH_MARGIN_MIN): string | null {
|
|
127
|
+
if (measure.medianSeconds === null) return null;
|
|
128
|
+
const windowSeconds = (Date.parse(publishAt) - Date.parse(cutAt)) / 1000;
|
|
129
|
+
if (windowSeconds >= measure.medianSeconds) return null;
|
|
130
|
+
const proposed = proposePublishAt(cutAt, measure, marginMin)!;
|
|
131
|
+
return `⚠️ 발행 시각이 실측 중앙값(${mins(measure.medianSeconds)}분)보다 짧다 — 컷→발행 ${mins(windowSeconds)}분 · 제안: ${kstHourMinute(proposed)} KST (최근 ${measure.samples.length}판 ${measure.samples.map((s) => s.version).join('·')} · +여유 ${marginMin}분)`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `release schedule show` 의 한 줄 — 제안 발행 시각 또는 «측정 없음». */
|
|
135
|
+
export function formatPublishProposal(cutAt: string, measure: PublishWindowMeasure, marginMin = DEFAULT_PUBLISH_MARGIN_MIN): string {
|
|
136
|
+
const proposed = proposePublishAt(cutAt, measure, marginMin);
|
|
137
|
+
if (!proposed || measure.medianSeconds === null) {
|
|
138
|
+
return `발행 제안 — 실측 없음(완료된 release-loop 런에서 발행 노드 시각을 못 읽었다 · 본 곳 ${measure.root}${measure.skipped.length ? ` · 못 잰 런 ${measure.skipped.length}` : ''})`;
|
|
139
|
+
}
|
|
140
|
+
return `발행 제안 ${kstHourMinute(proposed)} KST — 컷 + 실측 중앙값 ${mins(measure.medianSeconds)}분(최근 ${measure.samples.length}판 ${measure.samples.map((s) => `${s.version} ${mins(s.seconds)}분`).join(' · ')}) + 여유 ${marginMin}분`;
|
|
141
|
+
}
|
|
@@ -114,10 +114,7 @@ export function recordDocsFollow(root: string, manifest: ReleaseManifest, items:
|
|
|
114
114
|
const journal = join(root, 'seat-requests', 'requests.jsonl');
|
|
115
115
|
withSeatRequestLedgerLock(journal, () => {
|
|
116
116
|
const known = new Set(listSeatRequests(root).map((row) => row.key));
|
|
117
|
-
const missing =
|
|
118
|
-
...report.pending.map((item) => ({ id: item.id, text: `문서 반영 대기 목록 v${manifest.version} · ${item.id}: ${item.missing.join(', ')} — 착지 ${item.sha}; docs·홈 실측 뒤 닫기` })),
|
|
119
|
-
...report.unassessed.map((id) => ({ id, text: `문서 반영 대기 목록 v${manifest.version} · ${id}: 착지·green·EXPOSE-RUBRIC 근거 미판정 — 확인 전 공개 문면 금지` })),
|
|
120
|
-
];
|
|
117
|
+
const missing = report.pending.map((item) => ({ id: item.id, text: `문서 반영 대기 목록 v${manifest.version} · ${item.id}: ${item.missing.join(', ')} — 착지 ${item.sha}; docs·홈 실측 뒤 닫기` }));
|
|
121
118
|
const rows = missing.filter((item) => !known.has(`docs-follow:${manifest.version}:${item.id}`)).map((item) => ({
|
|
122
119
|
key: `docs-follow:${manifest.version}:${item.id}`, seat: 'MK', status: 'queued' as const,
|
|
123
120
|
text: item.text, queuedAt: now.toISOString(), ref: `release:${manifest.version}:docs-follow`,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// RELEASE-LEDGER-UNIVERSE (0.2.20 P0): the release run decides its universe ONCE at the entry and every node
|
|
2
|
+
// subprocess is pinned to it (graph runner `pinChildUniverse` stamps ELANOUS_STATE_DIR = this root). Without it a node
|
|
3
|
+
// launched as `bun scripts/release-loop/*-node.ts` from a non-leader tree (wt-release) re-resolved the tree-derived test
|
|
4
|
+
// universe and wrote manifest·story·gate logs to ⟨test:wt-release⟩ while the run ledger was prod (10-07).
|
|
5
|
+
import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
6
|
+
import { debug } from '../../src/debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot, normRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
|
|
9
|
+
export interface ReleaseUniverseDeps {
|
|
10
|
+
/** An injected ledger root (tests, or a caller that already decided) is used as is. */
|
|
11
|
+
root?: string;
|
|
12
|
+
override?: () => string | undefined;
|
|
13
|
+
prodRoot?: () => string;
|
|
14
|
+
effectiveRoot?: () => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The single universe of a release run. An explicit `--config-dir` selects its own universe; otherwise the release
|
|
19
|
+
* ledger is the machine-wide prod root, and a process that itself resolved elsewhere (a source tree without a stamp)
|
|
20
|
+
* is refused before any node starts — its own logs would land in a different universe from its nodes.
|
|
21
|
+
*/
|
|
22
|
+
export function releaseRunUniverse(deps: ReleaseUniverseDeps = {}): string {
|
|
23
|
+
if (deps.root) return deps.root;
|
|
24
|
+
const override = (deps.override ?? getElanousConfigDirOverride)();
|
|
25
|
+
if (override) {
|
|
26
|
+
const root = (deps.effectiveRoot ?? effectiveInstanceRoot)();
|
|
27
|
+
debug.log('release.run', 'universe', { root, layer: 'explicit-flag' });
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
const prod = normRoot((deps.prodRoot ?? prodInstanceRoot)());
|
|
31
|
+
const parent = normRoot((deps.effectiveRoot ?? effectiveInstanceRoot)());
|
|
32
|
+
if (parent !== prod) {
|
|
33
|
+
debug.log('release.run', 'universe-refused', { parent, prod });
|
|
34
|
+
throw new Error(`release run refused: this process resolves to ${parent}, not the release ledger universe ${prod} — its nodes would split ledgers and logs across universes. Run the installed \`elanous\` or pass \`--config-dir ${prod}\``);
|
|
35
|
+
}
|
|
36
|
+
debug.log('release.run', 'universe', { root: prod, layer: 'prod' });
|
|
37
|
+
return prod;
|
|
38
|
+
}
|