elanous 0.2.14 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.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 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- 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/loops/index.html +1 -1
- package/apps/pwa/out/loops/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/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +187 -40
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { FEATURE_MATURITY } from '../../src/maturity/feature-maturity.js';
|
|
4
|
+
import type { ChecklistItem } from '../../src/release-loop/checklist.js';
|
|
5
|
+
import type { ReleaseSchedule } from '../../src/release-loop/release-schedule.js';
|
|
6
|
+
import type { MergedRunLedgerQuery } from '../../src/self-implement/run-ledger.js';
|
|
7
|
+
import { generateFeatureMap } from './generate-feature-map.js';
|
|
8
|
+
|
|
9
|
+
const MAP = resolve(import.meta.dir, '../../docs/marketing/MAP-value-props-to-features.md');
|
|
10
|
+
const AUTONOMY = new Set(['ORCH1', 'ORCH2', 'LOOP-LIVE1', 'AUTOQ-LOOP', 'AUTHOR-PAR', 'RUN-HELPER', 'FINISH-RATE']);
|
|
11
|
+
const ROADMAP = /^(?:ENT-|KPACK|GRID)/;
|
|
12
|
+
|
|
13
|
+
export interface AxBriefSources {
|
|
14
|
+
featureMap: () => string;
|
|
15
|
+
checklist: (version: string) => readonly ChecklistItem[];
|
|
16
|
+
schedules: () => readonly ReleaseSchedule[];
|
|
17
|
+
merges: () => Pick<MergedRunLedgerQuery, 'entries' | 'ledgerDirectoryMissing' | 'unreadableLedgerCount' | 'excludedLedgerCount'>;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Source<T> = { ok: true; value: T } | { ok: false; reason: string };
|
|
22
|
+
|
|
23
|
+
function readSource<T>(read: () => T): Source<T> {
|
|
24
|
+
try { return { ok: true, value: read() }; }
|
|
25
|
+
catch (error) { return { ok: false, reason: error instanceof Error ? error.message : String(error) }; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cell(text: string): string {
|
|
29
|
+
return text.replace(/\|/g, '\\|').replace(/[\r\n]+/g, ' ').trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function reason(source: Source<unknown>): string {
|
|
33
|
+
return source.ok ? '' : `못 읽음 · 사유: ${cell(source.reason)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function versionNumbers(version: string): number[] | null {
|
|
37
|
+
return /^\d+\.\d+\.\d+$/.test(version) ? version.split('.').map(Number) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function after03(version: string): boolean {
|
|
41
|
+
const parts = versionNumbers(version);
|
|
42
|
+
return !!parts && (parts[0]! > 0 || parts[1]! >= 3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareVersions(left: string, right: string): number {
|
|
46
|
+
const a = versionNumbers(left)!, b = versionNumbers(right)!;
|
|
47
|
+
return a[0]! - b[0]! || a[1]! - b[1]! || a[2]! - b[2]!;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function firstLine(evidence: string | undefined): string {
|
|
51
|
+
return evidence?.split(/\r?\n/, 1)[0]?.trim() || '근거 없음';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read-only report: failures in one ledger do not turn other sections into claims of success. */
|
|
55
|
+
export function generateAxBrief(version: string, sources: AxBriefSources): string {
|
|
56
|
+
const now = (sources.now ?? (() => new Date()))().toISOString();
|
|
57
|
+
const feature = readSource(sources.featureMap);
|
|
58
|
+
const current = readSource(() => sources.checklist(version));
|
|
59
|
+
const schedules = readSource(sources.schedules);
|
|
60
|
+
const merges = readSource(sources.merges);
|
|
61
|
+
const mergeCount = merges.ok && !merges.value.ledgerDirectoryMissing
|
|
62
|
+
? new Set(merges.value.entries.filter((entry) => entry.merged).map((entry) => entry.prNumber)).size : null;
|
|
63
|
+
const mergeValue = merges.ok && !merges.value.ledgerDirectoryMissing && merges.value.unreadableLedgerCount > 0
|
|
64
|
+
? `못 읽음 · 사유: 원장 ${merges.value.unreadableLedgerCount}건 판독 실패 · 확인된 병합 ${mergeCount}건 (집계 불완전)`
|
|
65
|
+
: mergeCount ?? (merges.ok ? '못 읽음 · 사유: 원장 디렉터리 없음' : reason(merges));
|
|
66
|
+
const mergeCaveat = merges.ok && !merges.value.ledgerDirectoryMissing && (merges.value.unreadableLedgerCount || merges.value.excludedLedgerCount)
|
|
67
|
+
? ` · 미반영 원장: 못 읽음 ${merges.value.unreadableLedgerCount}건 · 중복 병합 제외 ${merges.value.excludedLedgerCount}건` : '';
|
|
68
|
+
const featureRows = feature.ok ? feature.value.split(/\r?\n/).filter((line) => /^\| .* \| (?:된다|베타) \|/.test(line)) : [];
|
|
69
|
+
const autonomous = current.ok ? current.value.filter((item) => AUTONOMY.has(item.id)) : [];
|
|
70
|
+
const roadmap = schedules.ok ? schedules.value.filter((schedule) => after03(schedule.version)).sort((a, b) => compareVersions(a.version, b.version)) : [];
|
|
71
|
+
const roadmapSources = roadmap.map((schedule) => ({ version: schedule.version, result: schedule.version === version ? current : readSource(() => sources.checklist(schedule.version)) }));
|
|
72
|
+
const roadmapFailures = roadmapSources.filter(({ result }) => !result.ok).map(({ version: v, result }) => `${v}: ${reason(result)}`);
|
|
73
|
+
const roadmapRows = roadmapSources.flatMap(({ version: v, result }) => result.ok
|
|
74
|
+
? result.value.filter((item) => ROADMAP.test(item.id)).map((item) => `| ${cell(v)} | ${cell(item.id)} | ${cell(item.title)} |`)
|
|
75
|
+
: []);
|
|
76
|
+
const owners = [...new Set(autonomous.map((item) => item.owner).filter((owner): owner is string => !!owner))];
|
|
77
|
+
return [
|
|
78
|
+
'# AX-BRIEF — 내부·파트너용 AX 브리핑',
|
|
79
|
+
'',
|
|
80
|
+
'> 공개 문서가 아닙니다. 내부 수치·자리 이름을 포함하며 외부 공개 판정으로 쓰지 않습니다.',
|
|
81
|
+
`> 생성: ${cell(version)} · ${now} · 원천: FEATURE-MAP 생성 출력, 릴리스 체크리스트, 릴리스 일정(release schedules), self-implement run-ledger`,
|
|
82
|
+
'',
|
|
83
|
+
'## 1. 지금 되는 것',
|
|
84
|
+
'',
|
|
85
|
+
feature.ok ? '| 기능 | 성숙도 | 칸 id | 근거 PR |' : reason(feature),
|
|
86
|
+
...(feature.ok ? ['|---|---|---|---|', ...featureRows] : []),
|
|
87
|
+
'',
|
|
88
|
+
'## 2. 자율 단계표',
|
|
89
|
+
'',
|
|
90
|
+
current.ok ? '| 칸 | 판 | 상태 | 근거 첫 줄 |' : reason(current),
|
|
91
|
+
...(current.ok ? ['|---|---|---|---|', ...autonomous.map((item) => `| ${cell(item.id)} | ${cell(version)} | ${cell(item.status)} | ${cell(firstLine(item.evidence))} |`)] : []),
|
|
92
|
+
...(current.ok ? [`자리 이름: ${owners.length ? owners.map(cell).join(' · ') : '원장에 없음'}`] : []),
|
|
93
|
+
'',
|
|
94
|
+
`실제 병합 수 (self-implement run-ledger 전체 기록 · 중복 PR 제외 · 외부/수동 병합 제외): ${mergeValue}${mergeCaveat}`,
|
|
95
|
+
'',
|
|
96
|
+
'## 3. 로드맵 계단',
|
|
97
|
+
'',
|
|
98
|
+
'> 계획 · 바뀔 수 있습니다',
|
|
99
|
+
schedules.ok ? '| 판 | 칸 | 제목 |' : reason(schedules),
|
|
100
|
+
...(schedules.ok ? ['|---|---|---|', ...roadmapRows, ...roadmapFailures.map((failure) => `못 읽음 · 사유: ${cell(failure)}`)] : []),
|
|
101
|
+
'',
|
|
102
|
+
'## 4. 도입 경로 (파일럿 기준 · 한 자리부터)',
|
|
103
|
+
'',
|
|
104
|
+
'1. **설치 한 줄 → 웹 앱 첫 화면** — 쓰던 구독을 그대로 알아본다.',
|
|
105
|
+
'2. **한 자리(예: 마케팅 또는 기술)만** — L1(한 줄 → 병합)을 그 팀 저장소에서. 측정: 발사 대비 병합 · 사람 수확 비율.',
|
|
106
|
+
'3. **결정 카드** — 비가역·공개만 사람에게. 나머지는 자리에 위임 범위를 적는다.',
|
|
107
|
+
'4. **자리 넷 ⊕ 그림자 루프** — L2 를 «기록만»으로 일주일 대조한 뒤 live.',
|
|
108
|
+
'5. **지식 팩** — 사내 문서·절차를 팩으로(로드맵 §3 · 유료 판매는 결정 뒤).',
|
|
109
|
+
'',
|
|
110
|
+
].join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (import.meta.main) {
|
|
114
|
+
const args = process.argv.slice(2);
|
|
115
|
+
const option = (name: string) => {
|
|
116
|
+
const index = args.indexOf(name);
|
|
117
|
+
if (index < 0) return undefined;
|
|
118
|
+
const value = args[index + 1];
|
|
119
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} 값이 필요하다`);
|
|
120
|
+
args.splice(index, 2);
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
const version = option('--version');
|
|
124
|
+
const out = option('--out');
|
|
125
|
+
if (!version || args.length) throw new Error('Usage: bun scripts/marketing/generate-ax-brief.ts --version <v> [--out <file>]');
|
|
126
|
+
const { listChecklist } = await import('../../src/release-loop/checklist.js');
|
|
127
|
+
const { listSchedules } = await import('../../src/release-loop/release-schedule.js');
|
|
128
|
+
const { queryMergedRunLedgers } = await import('../../src/self-implement/run-ledger.js');
|
|
129
|
+
const { releaseLedgerRoot } = await import('../../src/instance/resolve.js');
|
|
130
|
+
const root = releaseLedgerRoot();
|
|
131
|
+
const checklist = (v: string) => listChecklist(v, root).items;
|
|
132
|
+
const body = generateAxBrief(version, {
|
|
133
|
+
featureMap: () => generateFeatureMap({ map: readFileSync(MAP, 'utf8'), items: checklist(version), registry: FEATURE_MATURITY, version }),
|
|
134
|
+
checklist,
|
|
135
|
+
schedules: () => listSchedules(root),
|
|
136
|
+
merges: () => queryMergedRunLedgers(),
|
|
137
|
+
});
|
|
138
|
+
if (out) writeFileSync(out, body);
|
|
139
|
+
else process.stdout.write(body);
|
|
140
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { FEATURE_MATURITY, type Maturity } from '../../src/maturity/feature-maturity.js';
|
|
4
|
+
import type { ChecklistItem } from '../../src/release-loop/checklist.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAP = resolve(import.meta.dir, '../../docs/marketing/MAP-value-props-to-features.md');
|
|
7
|
+
const HELD = /특허\s*(?:보류|후보)|GOAL-CASCADE/i;
|
|
8
|
+
const PR = /(?:^|[^\w])#(\d+)\b|\/pull\/(\d+)\b/g;
|
|
9
|
+
type Grade = '된다' | '베타' | '로드맵';
|
|
10
|
+
type Row = { feature: string; grade: Grade; id: string; pr: string };
|
|
11
|
+
type Registry = typeof FEATURE_MATURITY;
|
|
12
|
+
|
|
13
|
+
function cells(line: string): string[] {
|
|
14
|
+
return line.slice(1, -1).split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\\|/g, '|'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clean(text: string): string {
|
|
18
|
+
return text.replace(/\*\*/g, '').replace(/\s+/g, ' ').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function gradeFor(feature: string, hint: string, registry: Registry): Maturity | undefined {
|
|
22
|
+
// A surface alone ("cli", "telegram") does not identify a feature in the registry.
|
|
23
|
+
const key = /\/(?:[\w-]+)(?:\/[\w-]+)*/.exec(hint)?.[0];
|
|
24
|
+
if (key && Object.prototype.hasOwnProperty.call(registry.pwaRoute, key))
|
|
25
|
+
return registry.pwaRoute[key as keyof Registry['pwaRoute']].pwa;
|
|
26
|
+
const named = /\b(cliRoot|tuiSlash|telegramCommand|discordCommand):([\w:-]+)/.exec(hint);
|
|
27
|
+
if (named) {
|
|
28
|
+
const group = registry[named[1] as Exclude<keyof Registry, 'pwaRoute'>] as Record<string, Maturity>;
|
|
29
|
+
return Object.prototype.hasOwnProperty.call(group, named[2]!) ? group[named[2]!] : undefined;
|
|
30
|
+
}
|
|
31
|
+
// Explicit command spelling in a feature name is unambiguous; a prose word is not.
|
|
32
|
+
const command = /`(?:elanous )?([\w:-]+)`/.exec(feature)?.[1];
|
|
33
|
+
if (command && Object.prototype.hasOwnProperty.call(registry.cliRoot, command))
|
|
34
|
+
return registry.cliRoot[command as keyof Registry['cliRoot']];
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function prs(text: string): string[] {
|
|
39
|
+
return [...new Set([...text.matchAll(PR)].map((m) => `#${m[1] ?? m[2]}`))];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeCell(text: string): string {
|
|
43
|
+
return text.replace(/\|/g, '\\|').replace(/[\r\n]+/g, ' ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Checklist cells require green status; rows without checklist cells use the map's maturity and public marker. */
|
|
47
|
+
export function generateFeatureMap(input: { map: string; items: readonly ChecklistItem[]; registry: Registry; version: string }): string {
|
|
48
|
+
const checklist = new Map(input.items.map((item) => [item.id, item]));
|
|
49
|
+
const green = new Map(input.items.filter((item) => item.status === 'green').map((item) => [item.id, item]));
|
|
50
|
+
const rows: Row[] = [];
|
|
51
|
+
let section = '';
|
|
52
|
+
for (const line of input.map.split(/\r?\n/)) {
|
|
53
|
+
if (/^#{2,3} /.test(line)) section = line;
|
|
54
|
+
if (!section.startsWith('## 2. 기능 배치표 ') || !line.startsWith('|')) continue;
|
|
55
|
+
const columns = cells(line);
|
|
56
|
+
if (columns.length !== 8 || !/^P\d+(?:[·⊕]P\d+)*$/.test(clean(columns[0]!))) continue;
|
|
57
|
+
const [_, rawFeature, rawId, , hint, publication, evidence] = columns;
|
|
58
|
+
const feature = clean(rawFeature!);
|
|
59
|
+
if (!feature || feature === '—') continue;
|
|
60
|
+
const id = clean(rawId!);
|
|
61
|
+
const ids = id.split(/[·⊕]/).map((part) => part.trim()).filter(Boolean);
|
|
62
|
+
const hasChecklistCell = ids.some((part) => checklist.has(part));
|
|
63
|
+
const matched = ids.map((part) => green.get(part)).filter((item): item is ChecklistItem => !!item);
|
|
64
|
+
const complete = ids.length > 0 && ids.every((part) => green.has(part));
|
|
65
|
+
const maturity = hasChecklistCell ? gradeFor(feature, hint!, input.registry) : undefined;
|
|
66
|
+
const mapMaturity = /^(?:\/\S+\s+)?(stable|beta)\b/i.exec(clean(hint!))?.[1]?.toLowerCase();
|
|
67
|
+
const grade: Grade = hasChecklistCell
|
|
68
|
+
? complete && maturity === 'stable' ? '된다' : complete && maturity === 'beta' ? '베타' : '로드맵'
|
|
69
|
+
: !clean(publication!).startsWith('✅') ? '로드맵'
|
|
70
|
+
: mapMaturity === 'stable' ? '된다' : mapMaturity === 'beta' ? '베타' : '로드맵';
|
|
71
|
+
const evidencePrs = hasChecklistCell
|
|
72
|
+
? [...new Set(matched.flatMap((item) => prs(item.evidence ?? '')))] : prs(evidence!);
|
|
73
|
+
const held = HELD.test([...columns, ...ids.map((part) => {
|
|
74
|
+
const item = checklist.get(part);
|
|
75
|
+
return item ? `${item.title} ${item.evidence ?? ''}` : '';
|
|
76
|
+
})].join(' '));
|
|
77
|
+
rows.push({ feature: held ? '개념' : feature, grade: held ? '로드맵' : grade,
|
|
78
|
+
id: held ? '—' : hasChecklistCell ? id : '지도 판정', pr: held || !evidencePrs.length ? '근거 없음'
|
|
79
|
+
: !hasChecklistCell || complete ? evidencePrs.join(', ') : `부분: ${evidencePrs.join(', ')} · 나머지 칸 미완` });
|
|
80
|
+
}
|
|
81
|
+
const lines = [
|
|
82
|
+
'# FEATURE-MAP — 엘라누스가 할 수 있는 것',
|
|
83
|
+
'',
|
|
84
|
+
`판: ${escapeCell(input.version)} · 원천: src/maturity/feature-maturity.ts + 릴리스 체크리스트 green + docs/marketing/MAP-value-props-to-features.md §2`,
|
|
85
|
+
`재생성: \`bun scripts/marketing/generate-feature-map.ts --version ${escapeCell(input.version)} --out docs/marketing/FEATURE-MAP.md\``,
|
|
86
|
+
'내부 배치용: 로드맵은 현재 기능이 아니며, 된다/베타도 공개 판정·컷의 조상 PR 확인을 대신하지 않는다.',
|
|
87
|
+
'',
|
|
88
|
+
'| 기능 | 성숙도 | 칸 id | 근거 PR |',
|
|
89
|
+
'|---|---|---|---|',
|
|
90
|
+
...rows.map((row) => `| ${escapeCell(row.feature)} | ${row.grade} | ${escapeCell(row.id)} | ${escapeCell(row.pr)} |`),
|
|
91
|
+
'',
|
|
92
|
+
];
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (import.meta.main) {
|
|
97
|
+
const args = process.argv.slice(2);
|
|
98
|
+
const option = (name: string) => {
|
|
99
|
+
const index = args.indexOf(name);
|
|
100
|
+
if (index < 0) return undefined;
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} 값이 필요하다`);
|
|
103
|
+
args.splice(index, 2);
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
const version = option('--version');
|
|
107
|
+
const mapFile = option('--map') ?? DEFAULT_MAP;
|
|
108
|
+
const checklistFile = option('--checklist');
|
|
109
|
+
const out = option('--out');
|
|
110
|
+
if (!version || args.length) throw new Error('Usage: bun scripts/marketing/generate-feature-map.ts --version <v> [--map <file>] [--checklist <snapshot.json>] [--out <file>]');
|
|
111
|
+
const items = checklistFile
|
|
112
|
+
? (JSON.parse(readFileSync(checklistFile, 'utf8')) as { items: ChecklistItem[] }).items
|
|
113
|
+
: (await import('../../src/release-loop/checklist.js')).listChecklist(version).items;
|
|
114
|
+
if (!Array.isArray(items)) throw new Error('체크리스트 items 배열이 없다');
|
|
115
|
+
const body = generateFeatureMap({ map: readFileSync(mapFile, 'utf8'), items, registry: FEATURE_MATURITY, version });
|
|
116
|
+
if (out) writeFileSync(out, body);
|
|
117
|
+
else process.stdout.write(body);
|
|
118
|
+
}
|
|
@@ -1,27 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { checklistGate, listChecklist, summarizeChecklist } from '../../src/release-loop/checklist.js';
|
|
3
|
+
import * as store from '../../src/release-loop/feature-store.js';
|
|
4
|
+
import { getSchedule } from '../../src/release-loop/release-schedule.js';
|
|
5
|
+
import { debug } from '../../src/debug/log.js';
|
|
3
6
|
import { emitNodeResult, errorResult, readGraphContext, type GraphContext } from './node-verdict.js';
|
|
4
7
|
|
|
5
|
-
export function runChecklistGate(context: GraphContext = readGraphContext()) {
|
|
8
|
+
export function runChecklistGate(context: GraphContext = readGraphContext(), now: Date = new Date()) {
|
|
6
9
|
const version = context.input.version;
|
|
7
10
|
const data = listChecklist(version);
|
|
11
|
+
const schedule = getSchedule(version);
|
|
12
|
+
const deadline = schedule?.landBy;
|
|
13
|
+
const overdue = deadline !== null && deadline !== undefined && now.getTime() > Date.parse(deadline);
|
|
14
|
+
const autoMoved = overdue ? data.items.filter((item) => item.status === 'yellow' && item.priority !== 'P0' && item.disposition === undefined).map((item) => item.id) : [];
|
|
8
15
|
const gate = checklistGate(version);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const p0 = data.items.filter((item) => item.status === 'yellow' && item.priority === 'P0').map((item) => item.id);
|
|
17
|
+
gate.undecided = gate.undecided.filter((id) => !autoMoved.includes(id) && !p0.includes(id));
|
|
18
|
+
gate.moved = gate.moved.filter((id) => !p0.includes(id)).concat(autoMoved);
|
|
19
|
+
gate.knownIssues = gate.knownIssues.filter((item) => !p0.includes(item.id));
|
|
20
|
+
gate.blocked.push(...p0.filter((id) => !gate.blocked.includes(id)));
|
|
21
|
+
gate.ok = gate.red.length === 0 && gate.undecided.length === 0 && gate.blocked.length === 0;
|
|
22
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
23
|
+
const next = `${major}.${minor}.${patch! + 1}`;
|
|
24
|
+
const carried: string[] = [];
|
|
25
|
+
let carryFailure: string | undefined;
|
|
26
|
+
if (gate.moved.length) {
|
|
27
|
+
const nextChecklist = listChecklist(next);
|
|
28
|
+
const existing = new Map(nextChecklist.items.map((item) => [item.id, item]));
|
|
29
|
+
const movedFromHere = new Set(nextChecklist.history.filter((entry) => entry.field === 'move' && entry.from === version).map((entry) => entry.id));
|
|
30
|
+
const sameCarry = new Set<string>();
|
|
15
31
|
for (const id of gate.moved) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const original = data.items.find((item) => item.id === id)!;
|
|
33
|
+
const destination = existing.get(id);
|
|
34
|
+
if (!destination) continue;
|
|
35
|
+
if (destination.title === original.title && destination.owner === original.owner && (movedFromHere.has(id) || destination.evidence?.split('\n').some((line) => line.startsWith(`${version}에서 이월`)))) {
|
|
36
|
+
sameCarry.add(id);
|
|
37
|
+
} else {
|
|
38
|
+
carryFailure = `이월 충돌: ${id} — 다음 판에 다른 칸이 같은 ID`;
|
|
39
|
+
debug.log('release-loop.checklist-gate', 'carry-collision', { id, from: version, to: next });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!carryFailure) for (const id of gate.moved) {
|
|
44
|
+
try {
|
|
45
|
+
if (sameCarry.has(id)) {
|
|
46
|
+
store.remove(version, id, 'release-loop.checklist-gate');
|
|
47
|
+
debug.log('release-loop.checklist-gate', 'carry-closed', { id, from: version, to: next });
|
|
48
|
+
} else {
|
|
49
|
+
const reason = autoMoved.includes(id) ? `컷 자동 이월 · 마감 ${new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(new Date(deadline!))}` : '컷 자동 이월 · 처분 move';
|
|
50
|
+
store.move(id, version, next, 'release-loop.checklist-gate', undefined, undefined, reason, { clearDisposition: true });
|
|
51
|
+
carried.push(id);
|
|
52
|
+
debug.log('release-loop.checklist-gate', 'carried', { id, from: version, to: next });
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
carryFailure = `이월 실패: ${id} — ${error instanceof Error ? error.message : String(error)}`;
|
|
56
|
+
break;
|
|
21
57
|
}
|
|
22
58
|
}
|
|
23
59
|
}
|
|
24
|
-
|
|
60
|
+
if (carryFailure) gate.ok = false;
|
|
61
|
+
const counts = summarizeChecklist(data);
|
|
62
|
+
const summary = `${carryFailure ? `${carryFailure} · ` : ''}확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}${gate.parity?.length ? ` · ⚠ 짝 경고 ${gate.parity.length}(${gate.parity.map((p) => p.id).join(', ')})` : ''} · 이월 ${carried.length}(${carried.join(', ') || '-'})`;
|
|
63
|
+
return { outcome: gate.ok ? 'ok' as const : 'fail' as const, verdict: gate.ok ? 'pass' as const : 'fail' as const, summary, ...gate, carried };
|
|
25
64
|
}
|
|
26
65
|
|
|
27
66
|
if (import.meta.main) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -19,10 +19,10 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
|
|
|
19
19
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
20
20
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
21
21
|
|
|
22
|
-
interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }
|
|
22
|
+
interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }>; timedOut?: boolean }
|
|
23
23
|
export interface GateRunner {
|
|
24
|
-
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
25
|
-
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
24
|
+
command(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
|
|
25
|
+
localCommand(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
|
|
26
26
|
sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
|
|
27
27
|
add(tree: string, commit: string): Promise<void>;
|
|
28
28
|
remove(tree: string): Promise<void>;
|
|
@@ -52,7 +52,7 @@ export interface GateResult {
|
|
|
52
52
|
fixed: number;
|
|
53
53
|
knownEnv: number;
|
|
54
54
|
knownEnvCleared: string[];
|
|
55
|
-
stalledEnv: Array<{ file: string; reason: 'no-output'; local: 'passed' | 'failed' }>;
|
|
55
|
+
stalledEnv: Array<{ file: string; reason: 'no-output' | 'isolated-timeout'; local: 'passed' | 'failed' | 'timeout' }>;
|
|
56
56
|
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
57
57
|
durationMs: number;
|
|
58
58
|
error?: string;
|
|
@@ -189,17 +189,63 @@ class StalledPodShards extends Error {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/** Isolated re-runs (host stall re-run · cut and baseline re-checks) are time-limited; sweeps and installs keep their full time. */
|
|
193
|
+
export const GATE_ISOLATED_TIMEOUT_MS = 300_000;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Runs a limited local command in its own process group. On timeout: SIGTERM the group (test-deterministic forwards it to its own
|
|
197
|
+
* `bun test` group — Chrome included), SIGKILL after a grace, and return after a hard deadline even if a foreign-group process still holds the pipe.
|
|
198
|
+
*/
|
|
199
|
+
export function limitedLocalCommand(cmd: string, args: string[], cwd: string, limitMs: number, graceMs = 10_000): Promise<CommandResult> {
|
|
200
|
+
return new Promise((resolveRun) => {
|
|
201
|
+
const chunks: string[] = [];
|
|
202
|
+
let timedOut = false;
|
|
203
|
+
let done = false;
|
|
204
|
+
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
205
|
+
const child = spawn(cmd, args, { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
206
|
+
let kept = 0;
|
|
207
|
+
// Same ceiling as the unlimited spawnSync path (maxBuffer 128MB): drop the oldest output beyond it, keep the tail.
|
|
208
|
+
const keep = (d: string) => { chunks.push(d); kept += d.length; while (kept > 128 * 1024 * 1024 && chunks.length > 1) kept -= chunks.shift()!.length; };
|
|
209
|
+
child.stdout?.setEncoding('utf8').on('data', keep);
|
|
210
|
+
child.stderr?.setEncoding('utf8').on('data', keep);
|
|
211
|
+
const signalGroup = (signal: NodeJS.Signals) => { try { if (child.pid) process.kill(-child.pid, signal); } catch { /* group already gone */ } };
|
|
212
|
+
const finish = (code: number | null, error?: Error) => {
|
|
213
|
+
if (done) return;
|
|
214
|
+
done = true;
|
|
215
|
+
for (const timer of timers) clearTimeout(timer);
|
|
216
|
+
child.stdout?.destroy();
|
|
217
|
+
child.stderr?.destroy();
|
|
218
|
+
const tail = error ? `\n${error}` : timedOut ? `\nkilled after ${limitMs}ms (process group)` : '';
|
|
219
|
+
resolveRun({ rc: code ?? 2, output: `${chunks.join('')}${tail}`, ...(timedOut ? { timedOut } : {}) });
|
|
220
|
+
};
|
|
221
|
+
timers.push(setTimeout(() => {
|
|
222
|
+
timedOut = true;
|
|
223
|
+
signalGroup('SIGTERM');
|
|
224
|
+
timers.push(setTimeout(() => signalGroup('SIGKILL'), graceMs));
|
|
225
|
+
timers.push(setTimeout(() => finish(null), graceMs + 5_000));
|
|
226
|
+
}, limitMs));
|
|
227
|
+
child.on('error', (error) => finish(null, error));
|
|
228
|
+
child.on('close', (code) => finish(code));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
192
232
|
export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
|
|
193
233
|
let remoteMirror: string | undefined;
|
|
194
|
-
const localCommand: GateRunner['
|
|
234
|
+
const localCommand: GateRunner['localCommand'] = async (cmd, args, cwd, limitMs) => {
|
|
235
|
+
if (limitMs) return limitedLocalCommand(cmd, args, cwd, limitMs);
|
|
195
236
|
const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
|
|
196
237
|
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
|
|
197
238
|
};
|
|
198
239
|
const command: GateRunner['command'] = commandOverride ?? (remote
|
|
199
|
-
? async (cmd, args, cwd) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
? async (cmd, args, cwd, limitMs) => {
|
|
241
|
+
// A limited remote run is killed on the remote host; the local ssh deadline is a backstop.
|
|
242
|
+
// GNU timeout signals its own process group: -s KILL would kill timeout too (ssh 255), so TERM first then KILL after 10s (124 / 137).
|
|
243
|
+
// No coreutils timeout on the remote → run unlimited there and rely on the local ssh deadline.
|
|
244
|
+
const limitPrefix = limitMs ? `$(command -v timeout >/dev/null 2>&1 && echo 'timeout -k 10 ${Math.ceil(limitMs / 1000)}') ` : '';
|
|
245
|
+
const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${limitPrefix}${[cmd, ...args].map(quote).join(' ')}`],
|
|
246
|
+
{ encoding: 'utf8', maxBuffer: 128 * 1024 * 1024, ...(limitMs ? { timeout: limitMs + 60_000, killSignal: 'SIGKILL' as const } : {}) });
|
|
247
|
+
const timedOut = !!limitMs && (run.status === 124 || run.status === 137 || (run.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT');
|
|
248
|
+
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}`, ...(timedOut ? { timedOut } : {}) };
|
|
203
249
|
}
|
|
204
250
|
: localCommand);
|
|
205
251
|
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
|
|
@@ -630,7 +676,15 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
630
676
|
}
|
|
631
677
|
for (const { file, reason } of stalled) {
|
|
632
678
|
if (!/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
|
|
633
|
-
|
|
679
|
+
// 0.2.15: InsidePage.test.tsx gave no Pod output and then hung on the host with headless Chrome — the run could only fail.
|
|
680
|
+
const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
681
|
+
// A file that stalls in its Pod shard and again on the host is an environment stall: record it, measure nothing from it.
|
|
682
|
+
if (isolated.timedOut) {
|
|
683
|
+
// Grandchildren (e.g. headless Chrome) can outlive the kill — the event lets the next release see a repeat.
|
|
684
|
+
debug.log('release-loop.gate', 'isolated-timeout', { file, label, limitMs: GATE_ISOLATED_TIMEOUT_MS });
|
|
685
|
+
local.push({ file, reason, local: 'timeout' });
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
634
688
|
const parsed = failuresOf(isolated, `${label} host isolated ${file}`);
|
|
635
689
|
if ([...parsed.failures, ...parsed.errors].some((id) => fileOf(id) !== file)) throw new Error(`host isolated run attributed to another file: ${file}`);
|
|
636
690
|
measured.failures.push(...parsed.failures);
|
|
@@ -688,14 +742,26 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
688
742
|
result.fixed = diff.fixed.length;
|
|
689
743
|
result.preexisting = diff.common.length;
|
|
690
744
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
691
|
-
|
|
745
|
+
// 0.2.15: the cut re-check of a new-failure file (InsidePage.test.tsx · headless Chrome) hung for good — limit it like the host re-run.
|
|
746
|
+
const cutCheck = cutHostFailed.has(file) ? undefined : await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
747
|
+
if (cutCheck?.timedOut) {
|
|
748
|
+
debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'cut isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
|
|
749
|
+
result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const isolatedCut = cutCheck === undefined
|
|
692
753
|
? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
|
|
693
|
-
: failuresOf(
|
|
754
|
+
: failuresOf(cutCheck, `cut isolated ${file}`);
|
|
694
755
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
695
756
|
const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
696
757
|
if (candidates.length === 0) continue;
|
|
697
758
|
const baselineTree = await getBaseTree();
|
|
698
|
-
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree);
|
|
759
|
+
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
760
|
+
if (previous.timedOut) {
|
|
761
|
+
debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'baseline isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
|
|
762
|
+
result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
699
765
|
let oldFailures: Set<string>;
|
|
700
766
|
if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
|
|
701
767
|
const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
|