elanous 0.2.11 → 0.2.13
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/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.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/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/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 +14 -14
- 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 +14 -14
- 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/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/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +5 -4
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import { streamLLM } from '../../src/llm.js';
|
|
9
|
+
import { unified } from 'unified';
|
|
10
|
+
import remarkParse from 'remark-parse';
|
|
11
|
+
import remarkGfm from 'remark-gfm';
|
|
12
|
+
import remarkRehype from 'remark-rehype';
|
|
13
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
14
|
+
import rehypeStringify from 'rehype-stringify';
|
|
15
|
+
|
|
16
|
+
export type Stage = 'collect' | 'draft' | 'render' | 'html' | 'pdf';
|
|
17
|
+
export interface Input { name: string; kind: 'pdf' | 'image' | 'markdown'; text: string }
|
|
18
|
+
export interface Counts { inputs: number; diagrams: number; rendered: number; html: 'yes' | 'no'; pdf: 'yes' | 'skipped' }
|
|
19
|
+
export interface StageResult extends Counts { outcome: 'ok' | 'skipped' }
|
|
20
|
+
export interface StageOptions {
|
|
21
|
+
llm?: (prompt: string) => Promise<string>;
|
|
22
|
+
toolsPath?: string;
|
|
23
|
+
referenceRoot?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ownRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../skills');
|
|
27
|
+
const kinds: Record<string, Input['kind']> = {
|
|
28
|
+
'.pdf': 'pdf', '.md': 'markdown', '.png': 'image', '.jpg': 'image', '.jpeg': 'image', '.webp': 'image',
|
|
29
|
+
};
|
|
30
|
+
const mermaid = /^```mermaid\s*\n([\s\S]*?)^```\s*$/gm;
|
|
31
|
+
const escapeHtml = (text: string) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
32
|
+
|
|
33
|
+
function pathStatus(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
34
|
+
try { return lstatSync(path); }
|
|
35
|
+
catch (error) {
|
|
36
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function outputDir(folder: string): string {
|
|
42
|
+
if (!lstatSync(folder).isDirectory()) throw new Error(`강의 폴더가 아니다: ${folder}`);
|
|
43
|
+
const dir = join(resolve(folder), 'lecture-note');
|
|
44
|
+
const status = pathStatus(dir);
|
|
45
|
+
if (status && (status.isSymbolicLink() || !status.isDirectory())) throw new Error('lecture-note 출력 폴더가 안전하지 않다');
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function artifact(dir: string, name: string): string {
|
|
51
|
+
const path = join(dir, name);
|
|
52
|
+
const status = pathStatus(path);
|
|
53
|
+
if (status && (status.isSymbolicLink() || !status.isFile())) throw new Error(`출력 파일이 안전하지 않다: ${path}`);
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function binary(name: string, path = process.env.PATH ?? ''): string | undefined {
|
|
58
|
+
for (const part of path.split(':').filter(Boolean)) {
|
|
59
|
+
const at = join(part, name);
|
|
60
|
+
if (existsSync(at) && statSync(at).isFile()) return at;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function run(bin: string, args: string[], dir: string, timeout: number): ReturnType<typeof spawnSync> {
|
|
66
|
+
return spawnSync(bin, args, { cwd: dir, encoding: 'utf8', timeout, maxBuffer: 20 * 1024 * 1024,
|
|
67
|
+
env: { ...process.env, HOME: dir, XDG_CACHE_HOME: dir, XDG_CONFIG_HOME: dir, UV_CACHE_DIR: dir, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readInputs(dir: string): Input[] {
|
|
71
|
+
const value: unknown = JSON.parse(readFileSync(join(dir, 'inputs.json'), 'utf8'));
|
|
72
|
+
if (!Array.isArray(value) || !value.every((item) => item && typeof item.name === 'string' && typeof item.kind === 'string' && typeof item.text === 'string')) {
|
|
73
|
+
throw new Error('inputs.json 형식 오류');
|
|
74
|
+
}
|
|
75
|
+
return value as Input[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function counts(dir: string): Counts {
|
|
79
|
+
const inputs = existsSync(join(dir, 'inputs.json')) ? readInputs(dir).length : 0;
|
|
80
|
+
const notes = existsSync(join(dir, 'notes.md')) ? readFileSync(join(dir, 'notes.md'), 'utf8') : '';
|
|
81
|
+
const diagrams = [...notes.matchAll(mermaid)].length;
|
|
82
|
+
const rendered = [...Array(diagrams).keys()].filter(i => existsSync(join(dir, `diagram-${i + 1}.png`))).length;
|
|
83
|
+
return { inputs, diagrams, rendered, html: existsSync(join(dir, 'notes.html')) ? 'yes' : 'no', pdf: existsSync(join(dir, 'notes.pdf')) ? 'yes' : 'skipped' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function collect(folder: string, options: StageOptions = {}): StageResult {
|
|
87
|
+
const dir = outputDir(folder);
|
|
88
|
+
const files: Input[] = [];
|
|
89
|
+
const pdfText = binary('pdftotext', options.toolsPath);
|
|
90
|
+
const sources = resolve(folder, 'sources/common');
|
|
91
|
+
const scan = [{ base: folder, prefix: '' }, ...(pathStatus(sources)?.isDirectory() && !pathStatus(sources)?.isSymbolicLink()
|
|
92
|
+
? [{ base: sources, prefix: 'sources/common/' }] : [])];
|
|
93
|
+
for (const { base, prefix } of scan) for (const entry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
94
|
+
const kind = kinds[extname(entry.name).toLowerCase()];
|
|
95
|
+
if (!entry.isFile() || !kind) continue;
|
|
96
|
+
const at = join(base, entry.name);
|
|
97
|
+
const text = kind === 'markdown' ? readFileSync(at, 'utf8') : kind === 'image' ? '이미지 — 시각 자료 (텍스트 추출 없음)' :
|
|
98
|
+
pdfText ? (() => {
|
|
99
|
+
const result = run(pdfText, ['-layout', at, '-'], dir, 60000);
|
|
100
|
+
return result.status === 0 ? String(result.stdout) : 'PDF 텍스트 없음 — pdftotext 추출 실패';
|
|
101
|
+
})() : 'PDF 텍스트 없음 — pdftotext 없음';
|
|
102
|
+
files.push({ name: prefix + entry.name, kind, text });
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(artifact(dir, 'inputs.json'), JSON.stringify(files, null, 2) + '\n');
|
|
105
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const instructions = `강의 자료로 종합 디지털 강의 노트 Markdown만 작성하라.
|
|
109
|
+
강의 진행 순서를 유지하고 손필기/슬라이드의 시각 정보와 요약의 텍스트를 통합하라. 읽지 못한 이미지는 추측하지 말고 출처를 표시하라.
|
|
110
|
+
다이어그램은 Mermaid 우선으로 해당 설명 위치에 \`\`\`mermaid 코드블록으로 내장하라 (곡선·자유형은 코드로 억지 변환하지 않는다).
|
|
111
|
+
비교·분류는 표로, 실제 사례는 별도 사례 박스로, 핵심 원칙은 인용문으로 표시하라.
|
|
112
|
+
수식은 KaTeX 인라인 \\( ... \\) 형식, 강조 수식은 > \\( ... \\) 형식으로 쓰고 $$ 블록 수식이나 볼드/코드 수식은 쓰지 마라.
|
|
113
|
+
강의 개요 바로 아래에 해당 주차 신규 용어집 표(| 약어 | 영문 | 한국어 | 정의 |)를 배치하라.
|
|
114
|
+
목차와 각 헤딩 위의 영문 앵커를 만들고, 맨 끝에 다이어그램 목록 부록을 둬라. 근거 없는 내용은 만들어내지 마라.`;
|
|
115
|
+
|
|
116
|
+
export async function draft(folder: string, options: StageOptions = {}): Promise<StageResult> {
|
|
117
|
+
const dir = outputDir(folder);
|
|
118
|
+
const inputs = readInputs(dir);
|
|
119
|
+
if (!inputs.length) throw new Error('강의 자료가 없다');
|
|
120
|
+
const prompt = `${instructions}\n\n자료:\n${inputs.map(i => `### ${i.name} (${i.kind})\n${i.text}`).join('\n\n')}`;
|
|
121
|
+
const ask = options.llm ?? ((request: string) => streamLLM([{ role: 'user', content: request }], () => {}));
|
|
122
|
+
const notes = (await ask(prompt)).trim();
|
|
123
|
+
if (!notes) throw new Error('LLM 초안이 비었다');
|
|
124
|
+
writeFileSync(artifact(dir, 'notes.md'), notes + '\n');
|
|
125
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function render(folder: string, options: StageOptions = {}): StageResult {
|
|
129
|
+
const dir = outputDir(folder);
|
|
130
|
+
const notes = readFileSync(join(dir, 'notes.md'), 'utf8');
|
|
131
|
+
const renderer = join(options.referenceRoot ?? ownRoot, 'diagram-master/references/engines/mermaid/render_mermaid.py');
|
|
132
|
+
const uv = binary('uv', options.toolsPath);
|
|
133
|
+
let unavailable = 0;
|
|
134
|
+
for (const [i, match] of [...notes.matchAll(mermaid)].entries()) {
|
|
135
|
+
const png = artifact(dir, `diagram-${i + 1}.png`);
|
|
136
|
+
if (existsSync(png)) rmSync(png);
|
|
137
|
+
if (!uv || !existsSync(renderer)) { unavailable++; continue; }
|
|
138
|
+
const mmd = artifact(dir, `diagram-${i + 1}.mmd`);
|
|
139
|
+
writeFileSync(mmd, match[1]!);
|
|
140
|
+
const result = run(uv, ['run', 'python', renderer, mmd, '--output', png], dir, 120000);
|
|
141
|
+
if (result.status !== 0 || !existsSync(png)) {
|
|
142
|
+
if (existsSync(png)) rmSync(png);
|
|
143
|
+
throw new Error(`Mermaid 렌더 실패 (diagram-${i + 1}, exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PNG 없음'}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (unavailable) console.log(`렌더러 없음: ${unavailable}`);
|
|
147
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function markdownHtml(text: string): string {
|
|
151
|
+
let suffix = 0;
|
|
152
|
+
while (text.includes(`LECTUREMATHOPEN${suffix}`) || text.includes(`LECTUREMATHCLOSE${suffix}`)) suffix++;
|
|
153
|
+
const open = `LECTUREMATHOPEN${suffix}`;
|
|
154
|
+
const close = `LECTUREMATHCLOSE${suffix}`;
|
|
155
|
+
const protectedText = text.replace(/\\\(/g, open).replace(/\\\)/g, close);
|
|
156
|
+
return String(unified().use(remarkParse).use(remarkGfm)
|
|
157
|
+
.use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).processSync(protectedText))
|
|
158
|
+
.replaceAll(open, '\\(').replaceAll(close, '\\)');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function html(folder: string, options: StageOptions = {}): StageResult {
|
|
162
|
+
const dir = outputDir(folder);
|
|
163
|
+
const notes = readFileSync(join(dir, 'notes.md'), 'utf8');
|
|
164
|
+
const template = readFileSync(join(options.referenceRoot ?? ownRoot, 'lecture-note-digitizer/references/html-template.md'), 'utf8');
|
|
165
|
+
const css = template.match(/```css\n([\s\S]*?)\n```/)?.[1];
|
|
166
|
+
if (!css) throw new Error('HTML 템플릿 CSS 없음');
|
|
167
|
+
let n = 0;
|
|
168
|
+
const withoutAppendix = notes.replace(/\n(?:---\n\n)?(?:<a id="appendix"><\/a>\n)?#+ 부록: 다이어그램 목록[\s\S]*$/u, '');
|
|
169
|
+
const anchored = withoutAppendix.replace(/^<a id="([a-zA-Z0-9-]+)"><\/a>$/gm, (_match, id: string) => `\n\nLECTURE_ANCHOR_${id}_END\n\n`);
|
|
170
|
+
const body = markdownHtml(anchored.replace(mermaid, (_block, source: string) => {
|
|
171
|
+
const png = `diagram-${++n}.png`;
|
|
172
|
+
return existsSync(join(dir, png)) ? `\n\n\n\n` : `\n\n\`\`\`mermaid\n${source}\`\`\`\n\n`;
|
|
173
|
+
})).replace(/<p><img src="(diagram-\d+\.png)" alt="(diagram-\d+)"><\/p>/g,
|
|
174
|
+
'<div class="diagram-container"><img src="$1" alt="$2"></div>')
|
|
175
|
+
.replace(/<p>LECTURE_ANCHOR_([a-zA-Z0-9-]+)_END<\/p>/g, '<a id="$1"></a>');
|
|
176
|
+
const title = notes.match(/^# (.+)$/m)?.[1] ?? '강의 노트';
|
|
177
|
+
const sections: string[] = [];
|
|
178
|
+
let heading = 0;
|
|
179
|
+
const numbered = body.replace(/<(h[12])>(.*?)<\/\1>/g, (_m, tag: string, label: string) => {
|
|
180
|
+
const id = `sec-${++heading}`;
|
|
181
|
+
sections.push(`<li><a href="#${id}">${label}</a></li>`);
|
|
182
|
+
return `<${tag} id="${id}">${label}</${tag}>`;
|
|
183
|
+
});
|
|
184
|
+
const page = `<!DOCTYPE html>\n<html lang="ko"><head><meta charset="UTF-8"><title>${escapeHtml(title)}</title>` +
|
|
185
|
+
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">' +
|
|
186
|
+
'<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>' +
|
|
187
|
+
'<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body,{delimiters:[{left:\'\\\\(\',right:\'\\\\)\',display:false}]});"></script>' +
|
|
188
|
+
`<style>${css}</style></head><body><div class="cover"><h1>${escapeHtml(title)}</h1></div>` +
|
|
189
|
+
`<div class="toc"><h2>목차</h2><ol>${sections.join('\n')}</ol></div><div class="section">${numbered}</div></body></html>\n`;
|
|
190
|
+
writeFileSync(artifact(dir, 'notes.html'), page);
|
|
191
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function pdf(folder: string, options: StageOptions = {}): StageResult {
|
|
195
|
+
const dir = outputDir(folder);
|
|
196
|
+
const source = artifact(dir, 'notes.html');
|
|
197
|
+
if (!existsSync(source)) throw new Error('notes.html 없음');
|
|
198
|
+
const converter = join(options.referenceRoot ?? ownRoot, 'lecture-note-digitizer/references/convert_to_pdf.py');
|
|
199
|
+
const python = binary('python3', options.toolsPath);
|
|
200
|
+
const target = artifact(dir, 'notes.pdf');
|
|
201
|
+
if (existsSync(target)) throw new Error('기존 notes.pdf 를 덮어쓰지 않는다');
|
|
202
|
+
const result = python && existsSync(converter) ? run(python, [converter, source], dir, 120000) : undefined;
|
|
203
|
+
const engineMissing = !result || (result.status !== 0 && !result.error &&
|
|
204
|
+
(/^ModuleNotFoundError: No module named ['"]playwright['"]$/m.test(String(result.stderr)) ||
|
|
205
|
+
/BrowserType\.launch: Executable doesn't exist at/.test(String(result.stderr))));
|
|
206
|
+
if (engineMissing) {
|
|
207
|
+
if (existsSync(target)) rmSync(target);
|
|
208
|
+
console.log('PDF 엔진 없음 — HTML 이 산출물');
|
|
209
|
+
return { ...counts(dir), pdf: 'skipped', outcome: 'skipped' };
|
|
210
|
+
}
|
|
211
|
+
if (result && (result.status !== 0 || !existsSync(target))) {
|
|
212
|
+
if (existsSync(target)) rmSync(target);
|
|
213
|
+
throw new Error(`PDF 변환 실패 (exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PDF 없음'}`);
|
|
214
|
+
}
|
|
215
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function runStage(stage: Stage, folder: string, options: StageOptions = {}): Promise<StageResult> {
|
|
219
|
+
const result = stage === 'collect' ? collect(folder, options) : stage === 'draft' ? await draft(folder, options) :
|
|
220
|
+
stage === 'render' ? render(folder, options) : stage === 'html' ? html(folder, options) : pdf(folder, options);
|
|
221
|
+
debug.log('lecture.note', stage, { inputs: result.inputs, diagrams: result.diagrams, rendered: result.rendered, pdf: result.pdf });
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function folderFromContext(): string {
|
|
226
|
+
const arg = process.argv.indexOf('--folder');
|
|
227
|
+
if (arg >= 0) {
|
|
228
|
+
const folder = process.argv[arg + 1];
|
|
229
|
+
if (!folder) throw new Error('--folder 값 없음');
|
|
230
|
+
return resolve(folder.replace(/^~(?=\/)/, homedir()));
|
|
231
|
+
}
|
|
232
|
+
const context = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
233
|
+
if (!context) throw new Error('input.folder 필요');
|
|
234
|
+
const data: unknown = JSON.parse(context.trim().startsWith('{') ? context : readFileSync(context, 'utf8'));
|
|
235
|
+
const folder = (data as { input?: { folder?: unknown } })?.input?.folder;
|
|
236
|
+
if (typeof folder !== 'string' || !folder.trim()) throw new Error('input.folder 필요');
|
|
237
|
+
const expanded = folder.replace(/^~(?=\/)/, homedir());
|
|
238
|
+
if (!isAbsolute(expanded)) throw new Error('input.folder 절대 경로 필요');
|
|
239
|
+
return resolve(expanded);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (import.meta.main) {
|
|
243
|
+
const stage = process.argv[2];
|
|
244
|
+
if (!['collect', 'draft', 'render', 'html', 'pdf'].includes(stage ?? '')) throw new Error('stage: collect|draft|render|html|pdf');
|
|
245
|
+
let folder = '';
|
|
246
|
+
try {
|
|
247
|
+
folder = folderFromContext();
|
|
248
|
+
const result = await runStage(stage as Stage, folder);
|
|
249
|
+
console.log(`lecture-note ${folder} inputs=${result.inputs} diagrams=${result.diagrams} rendered=${result.rendered} html=${result.html} pdf=${result.pdf}`);
|
|
250
|
+
console.log(JSON.stringify({ outcome: result.outcome }));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(error);
|
|
253
|
+
console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
export function parseInjectArgs(
|
|
4
|
+
argv: string[],
|
|
5
|
+
env: { TELEGRAM_TEST_BOT?: string | undefined },
|
|
6
|
+
): { to: string; text?: string; photos: string[]; caption?: string } {
|
|
7
|
+
const value = (flag: string): string | undefined => {
|
|
8
|
+
const index = argv.indexOf(flag);
|
|
9
|
+
if (index < 0) return undefined;
|
|
10
|
+
const next = argv[index + 1];
|
|
11
|
+
if (!next || next.startsWith('--')) throw new Error(`옵션 값 없음: ${flag}`);
|
|
12
|
+
return next;
|
|
13
|
+
};
|
|
14
|
+
const to = value('--to') ?? env.TELEGRAM_TEST_BOT;
|
|
15
|
+
const text = value('--text');
|
|
16
|
+
const photos: string[] = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
if (argv[i] !== '--photo') continue;
|
|
19
|
+
const photo = argv[++i];
|
|
20
|
+
if (!photo || photo.startsWith('--') || !existsSync(photo)) {
|
|
21
|
+
throw new Error(`사진 파일 경로 없음: ${photo ?? '(미지정)'}`);
|
|
22
|
+
}
|
|
23
|
+
photos.push(photo);
|
|
24
|
+
}
|
|
25
|
+
if (!to || (!text && photos.length === 0)) {
|
|
26
|
+
throw new Error('사용: telegram-inject.ts --to @monad_test_bot --text "<objective>"');
|
|
27
|
+
}
|
|
28
|
+
return photos.length > 0
|
|
29
|
+
? { to, photos, ...(text !== undefined ? { caption: text } : {}) }
|
|
30
|
+
: { to, text, photos };
|
|
31
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Seven-day read-only measurement of self-implement Pod memory by goal kind. */
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { LogStore, logsDbPath, type LogStoreRow } from '../src/mss/logging/log-store.js';
|
|
5
|
+
|
|
6
|
+
export const GOAL_TYPES = ['test', 'code', 'pwa-build', 'docs'] as const;
|
|
7
|
+
export type GoalType = typeof GOAL_TYPES[number];
|
|
8
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const MIB = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
type Observation = Pick<LogStoreRow, 'ts_ms' | 'event' | 'data' | 'category'>;
|
|
12
|
+
type Run = { type: GoalType | null; limit: string | null; peakMiB: number | null; samples: number; terminated: boolean; oomKilled: boolean };
|
|
13
|
+
type MemorySample = { cgroupBytes?: unknown };
|
|
14
|
+
export type MemoryMeasurement = {
|
|
15
|
+
window: { since: string; until: string };
|
|
16
|
+
sources: { logs: string; observations: number; samples: number; unclassifiedRuns: number; classification: 'goal-fields-or-space-id'; peakCoverage: 'recorded-samples-only' };
|
|
17
|
+
byGoalType: Array<{ goalType: GoalType; runs: number; measured: number; missingPeak: number; samples: number; memoryLimits: Record<string, number>; terminations: number; oomKilled: number; peakMiB: { p50: number | null; p95: number | null; max: number | null } }>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function payload(raw: string | null): Record<string, unknown> {
|
|
21
|
+
try {
|
|
22
|
+
const parsed: unknown = JSON.parse(raw ?? 'null');
|
|
23
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
24
|
+
} catch { return {}; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Infer only unambiguous kinds from free text; never turn an unknown title into code. */
|
|
28
|
+
export function goalTypeOf(description: string): GoalType | null {
|
|
29
|
+
const kinds: GoalType[] = [];
|
|
30
|
+
if (/\b(?:pwa|next\.js|frontend|front-end|web app)[-\s]+(?:build|compile|typecheck|bundle)\b/i.test(description)) kinds.push('pwa-build');
|
|
31
|
+
if (/\b(?:test|tests|testing|spec|coverage|gate)\b/i.test(description) || /시험|테스트|검증/.test(description)) kinds.push('test');
|
|
32
|
+
if (/\b(?:docs?|documentation|readme|manual|report)\b/i.test(description) || /문서|보고서/.test(description)) kinds.push('docs');
|
|
33
|
+
if (/\b(?:implement|implementation|coding|code|cli)\b/i.test(description) || /구현|코드/.test(description)) kinds.push('code');
|
|
34
|
+
return kinds.length === 1 ? kinds[0]! : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function recordedGoalType(data: Record<string, unknown>, spaceId: string): GoalType | null {
|
|
38
|
+
if ('goalType' in data) return GOAL_TYPES.find((type) => type === data.goalType) ?? null;
|
|
39
|
+
if ('goalKind' in data) return GOAL_TYPES.find((type) => type === data.goalKind) ?? null;
|
|
40
|
+
const descriptions = [data.goalTitle, data.goalDescription, data.feature].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
|
41
|
+
return descriptions.length ? goalTypeOf(descriptions.join(' ')) : goalTypeOf(spaceId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function percentile(sorted: readonly number[], fraction: number): number | null {
|
|
45
|
+
if (!sorted.length) return null;
|
|
46
|
+
const index = (sorted.length - 1) * fraction;
|
|
47
|
+
const low = Math.floor(index);
|
|
48
|
+
return Math.round((sorted[low]! + (sorted[Math.ceil(index)]! - sorted[low]!) * (index - low)) * 100) / 100;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** All rows are read before aggregation: log query's default 100-row limit is not a seven-day census. */
|
|
52
|
+
export function measurePodMemoryByGoal(rows: readonly Observation[], nowMs: number = Date.now(), logs = logsDbPath()): MemoryMeasurement {
|
|
53
|
+
const sinceMs = nowMs - WEEK_MS;
|
|
54
|
+
const windowRows = rows.filter((row) => row.ts_ms >= sinceMs && row.ts_ms <= nowMs && row.category === 'self-implement.pod');
|
|
55
|
+
const runs = new Map<string, Run>();
|
|
56
|
+
const jobToSpace = new Map<string, string>();
|
|
57
|
+
let samples = 0;
|
|
58
|
+
const ordered = [...windowRows].sort((a, b) => a.ts_ms - b.ts_ms);
|
|
59
|
+
for (const row of ordered) {
|
|
60
|
+
if (row.event !== 'memory-limit') continue;
|
|
61
|
+
const data = payload(row.data);
|
|
62
|
+
const id = typeof data.spaceId === 'string' ? data.spaceId : '';
|
|
63
|
+
if (!id) continue;
|
|
64
|
+
runs.set(id, { type: recordedGoalType(data, id), limit: typeof data.memoryLimit === 'string' ? data.memoryLimit : null,
|
|
65
|
+
peakMiB: null, samples: 0, terminated: false, oomKilled: false });
|
|
66
|
+
}
|
|
67
|
+
for (const row of ordered) {
|
|
68
|
+
const data = payload(row.data);
|
|
69
|
+
const job = typeof data.job === 'string' ? data.job : '';
|
|
70
|
+
const directId = typeof data.spaceId === 'string' ? data.spaceId : '';
|
|
71
|
+
if (row.event === 'job-applied' && job && runs.has(directId)) jobToSpace.set(job, directId);
|
|
72
|
+
const run = runs.get(directId || jobToSpace.get(job) || '');
|
|
73
|
+
if (!run) continue;
|
|
74
|
+
if (row.event === 'job-finished') {
|
|
75
|
+
run.terminated = true;
|
|
76
|
+
run.oomKilled ||= data.containerReason === 'OOMKilled';
|
|
77
|
+
}
|
|
78
|
+
if (row.event === 'oom-evidence') { run.terminated = true; run.oomKilled = true; }
|
|
79
|
+
if (data.reason === 'OOMKilled') { run.terminated = true; run.oomKilled = true; }
|
|
80
|
+
if (row.event !== 'oom-evidence' && row.event !== 'memory-last') continue;
|
|
81
|
+
const rawSamples: unknown[] = row.event === 'oom-evidence' && Array.isArray(data.samples) ? data.samples : [data.sample];
|
|
82
|
+
for (const raw of rawSamples) {
|
|
83
|
+
const record = raw && typeof raw === 'object' ? raw as MemorySample : {};
|
|
84
|
+
const bytes = record.cgroupBytes;
|
|
85
|
+
if (typeof bytes !== 'number' || !Number.isSafeInteger(bytes) || bytes < 0) continue;
|
|
86
|
+
const reading = bytes / MIB;
|
|
87
|
+
run.peakMiB = Math.max(run.peakMiB ?? 0, reading);
|
|
88
|
+
run.samples++;
|
|
89
|
+
samples++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
window: { since: new Date(sinceMs).toISOString(), until: new Date(nowMs).toISOString() },
|
|
94
|
+
sources: { logs, observations: windowRows.length, samples, unclassifiedRuns: [...runs.values()].filter((run) => run.type === null).length,
|
|
95
|
+
classification: 'goal-fields-or-space-id', peakCoverage: 'recorded-samples-only' },
|
|
96
|
+
byGoalType: GOAL_TYPES.map((goalType) => {
|
|
97
|
+
const group = [...runs.values()].filter((run) => run.type === goalType);
|
|
98
|
+
const peaks = group.flatMap((run) => run.peakMiB === null ? [] : [run.peakMiB]).sort((a, b) => a - b);
|
|
99
|
+
const memoryLimits: Record<string, number> = {};
|
|
100
|
+
for (const run of group) if (run.limit) memoryLimits[run.limit] = (memoryLimits[run.limit] ?? 0) + 1;
|
|
101
|
+
return { goalType, runs: group.length, measured: peaks.length, missingPeak: group.length - peaks.length,
|
|
102
|
+
samples: group.reduce((count, run) => count + run.samples, 0), memoryLimits,
|
|
103
|
+
terminations: group.filter((run) => run.terminated).length, oomKilled: group.filter((run) => run.oomKilled).length,
|
|
104
|
+
peakMiB: { p50: percentile(peaks, 0.5), p95: percentile(peaks, 0.95), max: peaks.at(-1) ?? null } };
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function readPodMemoryMeasurement(path: string = logsDbPath(), nowMs: number = Date.now()): MemoryMeasurement {
|
|
110
|
+
if (!existsSync(path)) throw new Error(`log store not found: ${path}`);
|
|
111
|
+
const store = LogStore.openReadOnly(path);
|
|
112
|
+
try {
|
|
113
|
+
return measurePodMemoryByGoal(store.queryAll({ exactCategories: ['self-implement.pod'], sinceMs: nowMs - WEEK_MS, untilMs: nowMs }), nowMs, path);
|
|
114
|
+
} finally { store.close(); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (import.meta.main) {
|
|
118
|
+
const args = process.argv.slice(2);
|
|
119
|
+
const usage = 'Usage: bun scripts/measure-pod-memory-by-goal.ts [--json] [--logs-db <existing-logs.db>] [--help]';
|
|
120
|
+
const dbArg = args.indexOf('--logs-db');
|
|
121
|
+
const db = dbArg === -1 ? logsDbPath() : args[dbArg + 1];
|
|
122
|
+
if (!db || (dbArg !== -1 && (db.startsWith('--') || args.lastIndexOf('--logs-db') !== dbArg))
|
|
123
|
+
|| args.some((arg, index) => !['--json', '--help', '--logs-db'].includes(arg) && index !== dbArg + 1)) throw new Error(usage);
|
|
124
|
+
if (args.includes('--help')) console.log(usage);
|
|
125
|
+
else {
|
|
126
|
+
const result = readPodMemoryMeasurement(db);
|
|
127
|
+
if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
|
|
128
|
+
else for (const group of result.byGoalType) console.log(`${group.goalType}: runs=${group.runs} measured=${group.measured} peak p50=${group.peakMiB.p50 ?? 'n/a'} MiB p95=${group.peakMiB.p95 ?? 'n/a'} MiB OOMKilled=${group.oomKilled}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -15,7 +15,7 @@ const outcomeMetric = (name: string, node: Record<string, unknown> | undefined):
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
/** Rows shown in auto-approval.md but never counted toward the 8 blocking metrics. */
|
|
18
|
-
const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
|
|
18
|
+
export const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
|
|
19
19
|
|
|
20
20
|
/** `ok` · `warn` (a failure) · `unmeasured` (error or no output) — the macOS smoke never blocks publishing. */
|
|
21
21
|
function macSmokeLevel(node: Record<string, unknown> | undefined): 'ok' | 'warn' | 'unmeasured' {
|
|
@@ -11,10 +11,11 @@ export function runChecklistGate(context: GraphContext = readGraphContext()) {
|
|
|
11
11
|
if (gate.ok && gate.moved.length) {
|
|
12
12
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
13
13
|
const next = `${major}.${minor}.${patch! + 1}`;
|
|
14
|
+
// 이월은 «같은 칸»을 다음 판으로 옮기는 것 — 다른 판 id 충돌 검사(CKID #23407)의 대상이 아니다.
|
|
14
15
|
for (const id of gate.moved) {
|
|
15
16
|
const item = data.items.find((candidate) => candidate.id === id)!;
|
|
16
17
|
if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
|
|
17
|
-
try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }); }
|
|
18
|
+
try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }, { allowDuplicateId: true }); }
|
|
18
19
|
catch (error) {
|
|
19
20
|
if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
|
|
20
21
|
}
|
|
@@ -18,7 +18,7 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
|
|
|
18
18
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
19
19
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
20
20
|
|
|
21
|
-
interface CommandResult { rc: number; output: string; passedIds?: string[] }
|
|
21
|
+
interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }> }
|
|
22
22
|
export interface GateRunner {
|
|
23
23
|
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
24
24
|
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
@@ -50,6 +50,7 @@ export interface GateResult {
|
|
|
50
50
|
fixed: number;
|
|
51
51
|
knownEnv: number;
|
|
52
52
|
knownEnvCleared: string[];
|
|
53
|
+
stalledEnv: Array<{ file: string; reason: 'no-output'; local: 'passed' | 'failed' }>;
|
|
53
54
|
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
54
55
|
durationMs: number;
|
|
55
56
|
error?: string;
|
|
@@ -124,7 +125,8 @@ export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-20
|
|
|
124
125
|
/** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
|
|
125
126
|
export const POD_SHARD_FILE_CAP = 27;
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
// review-model-ab: Pod 에서 출력 없이 멈춤 2/2(10-04 0.2.11 · 로컬 단독 4/0 · 1.7s) — GATE-STALL 수리 전까지 Pod 밖.
|
|
129
|
+
export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts', 'scripts/review-model-ab.test.ts'];
|
|
128
130
|
/** Nightly-only whole-repository cases; gate runs the fixture cases in these same files. */
|
|
129
131
|
export const GATE_NIGHTLY_AUDITS: readonly string[] = [
|
|
130
132
|
// Replaces F12's real-repo bucket B apps/pwa scope check and its whole-repo export partition check.
|
|
@@ -267,7 +269,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
267
269
|
}, 0);
|
|
268
270
|
const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
|
|
269
271
|
const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
|
|
270
|
-
const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
272
|
+
const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
271
273
|
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
272
274
|
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
273
275
|
// 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
|
|
@@ -321,13 +323,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
321
323
|
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
322
324
|
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
323
325
|
const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
|
|
326
|
+
const errors = clean === undefined ? NaN : summaryCount(clean, 'error');
|
|
324
327
|
let reason: ShardReason | undefined;
|
|
325
328
|
let namedFailures: number | undefined;
|
|
326
329
|
let unattributedDetail: string | undefined;
|
|
327
330
|
if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
|
|
328
331
|
else if (!clean || rc === undefined) reason = 'no-output';
|
|
329
332
|
else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
|
|
330
|
-
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length
|
|
333
|
+
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length
|
|
334
|
+
|| (Number(ran[1]) === 0 && !(errors > 0))) reason = 'incomplete';
|
|
331
335
|
else {
|
|
332
336
|
try {
|
|
333
337
|
const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
|
|
@@ -354,6 +358,11 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
354
358
|
}
|
|
355
359
|
}
|
|
356
360
|
}
|
|
361
|
+
if (reason === 'no-output' && !retriedNoOutput) {
|
|
362
|
+
const retry = await runShard(paths, shard, depth, `${branch}-retry`, true);
|
|
363
|
+
debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
|
|
364
|
+
return retry;
|
|
365
|
+
}
|
|
357
366
|
if (reason) {
|
|
358
367
|
// 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
|
|
359
368
|
// 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
|
|
@@ -394,7 +403,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
394
403
|
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
395
404
|
}
|
|
396
405
|
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
397
|
-
errors: Number.isNaN(
|
|
406
|
+
errors: Number.isNaN(errors) ? 0 : errors,
|
|
398
407
|
ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
399
408
|
};
|
|
400
409
|
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
@@ -407,11 +416,12 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
407
416
|
const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
|
|
408
417
|
errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
|
|
409
418
|
{ pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
|
|
410
|
-
if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
|
|
411
|
-
if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
|
|
419
|
+
if (stalledShards.some((item) => item.reason !== 'no-output' || item.files.length !== 1)) throw new StalledPodShards(stalledShards, total);
|
|
420
|
+
if (total.ran === 0 && total.errors === 0 && !stalledShards.length) throw new Error('sweep incomplete: no tests ran');
|
|
412
421
|
const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
|
|
413
422
|
return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n`,
|
|
414
|
-
passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort()
|
|
423
|
+
passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort(),
|
|
424
|
+
stalledEnv: stalledShards.flatMap((item) => item.files.map((file) => ({ file, reason: 'no-output' as const }))) };
|
|
415
425
|
};
|
|
416
426
|
return {
|
|
417
427
|
command,
|
|
@@ -544,7 +554,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
544
554
|
return baselineCommit(root, version);
|
|
545
555
|
};
|
|
546
556
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
547
|
-
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
|
|
557
|
+
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: 0 };
|
|
548
558
|
let work: string | undefined;
|
|
549
559
|
let baseSnapshot = false;
|
|
550
560
|
let cutFailures: SweepFailures | undefined;
|
|
@@ -595,7 +605,38 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
595
605
|
for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
|
|
596
606
|
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
|
|
597
607
|
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
|
|
598
|
-
const
|
|
608
|
+
const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
|
|
609
|
+
const stalled = run.stalledEnv ?? [];
|
|
610
|
+
const noCompletedShard = stalled.length && run.rc === 0
|
|
611
|
+
&& /(?:^|\n)0 pass\n0 fail\n0 errors\nRan 0 tests across 0 files\./.test(run.output);
|
|
612
|
+
const measured: SweepFailures = noCompletedShard ? { failures: [], errors: [] } : failuresOf(run, `${label} sweep`);
|
|
613
|
+
if (!stalled.length) return measured;
|
|
614
|
+
let hostTree = tree;
|
|
615
|
+
const local: GateResult['stalledEnv'] = [];
|
|
616
|
+
try {
|
|
617
|
+
if (opts.remote) {
|
|
618
|
+
hostTree = join(mkdtempSync(join(tmpdir(), 'release-gate-local-')), 'cut');
|
|
619
|
+
check(await runner.localCommand('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, hostTree], repo), 'host cut clone');
|
|
620
|
+
check(await runner.localCommand('git', ['checkout', '--quiet', '--detach', commit], hostTree), 'host cut checkout');
|
|
621
|
+
for (const dir of [hostTree, join(hostTree, 'apps/pwa')]) check(await runner.localCommand('bun', ['install'], dir), `host bun install ${dir}`);
|
|
622
|
+
}
|
|
623
|
+
for (const { file, reason } of stalled) {
|
|
624
|
+
if (!/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
|
|
625
|
+
const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree);
|
|
626
|
+
const parsed = failuresOf(isolated, `${label} host isolated ${file}`);
|
|
627
|
+
if ([...parsed.failures, ...parsed.errors].some((id) => fileOf(id) !== file)) throw new Error(`host isolated run attributed to another file: ${file}`);
|
|
628
|
+
measured.failures.push(...parsed.failures);
|
|
629
|
+
measured.errors.push(...parsed.errors);
|
|
630
|
+
local.push({ file, reason, local: parsed.failures.length || parsed.errors.length ? 'failed' : 'passed' });
|
|
631
|
+
}
|
|
632
|
+
} finally {
|
|
633
|
+
if (opts.remote && hostTree !== tree) rmSync(dirname(hostTree), { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
result.stalledEnv.push(...local);
|
|
636
|
+
debug.log('release-loop.gate', 'pod-shard-stalled-env', { files: stalled.map(({ file, reason }) => ({ file, reason })), local });
|
|
637
|
+
return measured;
|
|
638
|
+
};
|
|
639
|
+
const cut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
|
|
599
640
|
cutFailures = cut;
|
|
600
641
|
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
601
642
|
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
@@ -624,22 +665,24 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
624
665
|
// hour of local baseline after a 23-minute Pod cut).
|
|
625
666
|
const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'),
|
|
626
667
|
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
|
|
627
|
-
baseline =
|
|
668
|
+
baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
|
|
628
669
|
}
|
|
629
670
|
for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
|
|
630
671
|
const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
|
|
631
672
|
const cutIds = [...cut.failures, ...cut.errors];
|
|
673
|
+
const cutHostFailed = new Set(cutRun.stalledEnv?.map(({ file }) => file) ?? []);
|
|
632
674
|
const baselineIds = [...baseline.failures, ...baseline.errors];
|
|
633
675
|
const countedCut = splitKnownEnv(cutIds, known);
|
|
634
676
|
const countedBaseline = splitKnownEnv(baselineIds, known);
|
|
635
677
|
result.knownEnv = countedCut.knownEnv.length;
|
|
636
|
-
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
|
|
678
|
+
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
|
|
637
679
|
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
638
680
|
result.fixed = diff.fixed.length;
|
|
639
681
|
result.preexisting = diff.common.length;
|
|
640
682
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
641
|
-
const
|
|
642
|
-
|
|
683
|
+
const isolatedCut = cutHostFailed.has(file)
|
|
684
|
+
? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
|
|
685
|
+
: failuresOf(await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree), `cut isolated ${file}`);
|
|
643
686
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
644
687
|
const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
645
688
|
if (candidates.length === 0) continue;
|
|
@@ -684,7 +727,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
684
727
|
if (opts.remote) runner.remoteMirror = undefined;
|
|
685
728
|
result.durationMs = Date.now() - start;
|
|
686
729
|
}
|
|
687
|
-
if (cutFailures && !result.stalledShards?.length) {
|
|
730
|
+
if (cutFailures && !result.stalledShards?.length && !result.stalledEnv.length) {
|
|
688
731
|
try {
|
|
689
732
|
for (const location of roots) {
|
|
690
733
|
const path = failureFile(location, opts.version);
|
|
@@ -708,7 +751,8 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
708
751
|
export function graphGateResult(result: GateResult, graph: boolean) {
|
|
709
752
|
return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
|
|
710
753
|
verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
|
|
711
|
-
summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
|
|
754
|
+
summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
|
|
755
|
+
+ (result.stalledEnv.length ? ` · 환경 멈춤 ${result.stalledEnv.length} (Pod 밖 통과 ${result.stalledEnv.filter((item) => item.local === 'passed').length})` : '') };
|
|
712
756
|
}
|
|
713
757
|
|
|
714
758
|
export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
|
|
@@ -766,7 +810,7 @@ if (import.meta.main) {
|
|
|
766
810
|
version = opts.version;
|
|
767
811
|
result = await judgeGate(opts);
|
|
768
812
|
} catch (error) {
|
|
769
|
-
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
|
|
813
|
+
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: Date.now() - start, error: String(error) };
|
|
770
814
|
}
|
|
771
815
|
if (result.error) console.error(result.error);
|
|
772
816
|
debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });
|