elanous 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +36 -2
- package/src/harness/harness-pod-dispatch.ts +25 -55
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/hq/hq.ts +8 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +112 -6
- package/src/intake-plane/digest.ts +48 -26
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +90 -3
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/goal-author.ts +19 -13
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/orchestrator.ts +138 -7
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +36 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public export import guard — a file the public export includes must not statically import
|
|
3
|
+
* a module the export excludes. 0.2.15 shipped that break: src/decisions/proact-meter.ts
|
|
4
|
+
* imported src/directives/directive-index (exclude `src/directives/directive-index*`) and the
|
|
5
|
+
* export-check PWA build failed.
|
|
6
|
+
*
|
|
7
|
+
* bun scripts/ci-public-export-import-gate.ts --changed-files a b … # exit 1 on a hit · 0 when clean
|
|
8
|
+
*
|
|
9
|
+
* Only string-literal specifiers are resolved. Non-literal specifiers (createRequire with a
|
|
10
|
+
* concatenated string, and the like) are counted as unseen — this check cannot see them.
|
|
11
|
+
* The public PWA build itself stays on the release export-check run; it is not run here.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, posix, resolve } from 'node:path';
|
|
16
|
+
type ExportSelectors = {
|
|
17
|
+
loadExportConfig: (root: string, file?: string) => { readonly include: readonly string[]; readonly exclude: readonly string[]; readonly replace: Readonly<Record<string, string>> };
|
|
18
|
+
selectExportFiles: (tracked: readonly string[], config: { readonly include: readonly string[]; readonly exclude: readonly string[]; readonly replace: Readonly<Record<string, string>> }) => string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* scripts/public-export.ts `loadExportConfig` (line 123) and `selectExportFiles` (line 140).
|
|
23
|
+
* Imported on call, not at load: that module imports typescript, and a tree that has not
|
|
24
|
+
* installed it must still be able to load this gate and report why it could not measure.
|
|
25
|
+
*/
|
|
26
|
+
async function exportSelectors(): Promise<ExportSelectors> {
|
|
27
|
+
return import('./public-export.js');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExportImportHit {
|
|
31
|
+
readonly file: string;
|
|
32
|
+
readonly line: number;
|
|
33
|
+
readonly target: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExportImportCheck {
|
|
37
|
+
readonly measured: boolean;
|
|
38
|
+
readonly hits: readonly ExportImportHit[];
|
|
39
|
+
readonly unseen: number;
|
|
40
|
+
readonly detail?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CODE_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
|
44
|
+
const FROM_CLAUSE = /\b(?:import|export)\s+(?:type\s+)?(?:[^'"\n;]*?\s+from\s+)?(['"])([^'"\n]+)\1/g;
|
|
45
|
+
const DYNAMIC_IMPORT = /\bimport\s*\(\s*(['"])([^'"\n]+)\1/g;
|
|
46
|
+
const NON_LITERAL_IMPORT = /\b(?:import|require|createRequire)\s*\(\s*(?!['"`])/g;
|
|
47
|
+
|
|
48
|
+
function trackedFiles(root: string): string[] | undefined {
|
|
49
|
+
const run = spawnSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8', timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
|
|
50
|
+
if (run.status !== 0) return undefined;
|
|
51
|
+
return String(run.stdout).split('\0').filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Specifier resolved against the importer's directory, then mapped onto a repo-relative path. */
|
|
55
|
+
function resolveSpecifier(importer: string, specifier: string): string | undefined {
|
|
56
|
+
if (!specifier.startsWith('.')) return undefined;
|
|
57
|
+
const raw = posix.normalize(posix.join(posix.dirname(importer), specifier));
|
|
58
|
+
return raw.startsWith('../') ? undefined : raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function candidatePaths(resolved: string): string[] {
|
|
62
|
+
const ext = posix.extname(resolved);
|
|
63
|
+
const bases = ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs'
|
|
64
|
+
? [resolved.slice(0, -ext.length) + '.ts', resolved.slice(0, -ext.length) + '.tsx', resolved]
|
|
65
|
+
: [resolved];
|
|
66
|
+
const out: string[] = [];
|
|
67
|
+
for (const base of bases) {
|
|
68
|
+
out.push(base);
|
|
69
|
+
if (!posix.extname(base)) {
|
|
70
|
+
out.push(`${base}.ts`, `${base}.tsx`, `${base}.js`, `${base}.mjs`);
|
|
71
|
+
out.push(posix.join(base, 'index.ts'), posix.join(base, 'index.tsx'), posix.join(base, 'index.js'));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function lineOf(text: string, index: number): number {
|
|
78
|
+
let line = 1;
|
|
79
|
+
for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++;
|
|
80
|
+
return line;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Changed files that the public manifest includes, scanned for a literal import of a tracked
|
|
85
|
+
* module that the same manifest leaves out. `unseen` counts non-literal import/require calls.
|
|
86
|
+
*/
|
|
87
|
+
export async function exportImportCheck(changedFiles: readonly string[], root: string): Promise<ExportImportCheck> {
|
|
88
|
+
const tracked = trackedFiles(root);
|
|
89
|
+
if (!tracked) return { measured: false, hits: [], unseen: 0, detail: 'git ls-files 실패' };
|
|
90
|
+
let included: Set<string>;
|
|
91
|
+
try {
|
|
92
|
+
const { loadExportConfig, selectExportFiles } = await exportSelectors();
|
|
93
|
+
included = new Set(selectExportFiles(tracked, loadExportConfig(root)));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return { measured: false, hits: [], unseen: 0, detail: error instanceof Error ? error.message : '매니페스트를 못 읽었다' };
|
|
96
|
+
}
|
|
97
|
+
const trackedSet = new Set(tracked);
|
|
98
|
+
const hits: ExportImportHit[] = [];
|
|
99
|
+
let unseen = 0;
|
|
100
|
+
for (const file of changedFiles) {
|
|
101
|
+
if (!included.has(file) || !CODE_FILE.test(file)) continue;
|
|
102
|
+
const abs = join(root, file);
|
|
103
|
+
if (!existsSync(abs)) continue;
|
|
104
|
+
let text: string;
|
|
105
|
+
try { text = readFileSync(abs, 'utf8'); } catch { continue; }
|
|
106
|
+
const seen = new Set<string>();
|
|
107
|
+
const consider = (index: number, specifier: string) => {
|
|
108
|
+
const resolvedSpec = resolveSpecifier(file, specifier);
|
|
109
|
+
if (!resolvedSpec) return;
|
|
110
|
+
const target = candidatePaths(resolvedSpec).find((candidate) => trackedSet.has(candidate));
|
|
111
|
+
if (!target || included.has(target)) return;
|
|
112
|
+
const key = `${index}\0${target}`;
|
|
113
|
+
if (seen.has(key)) return;
|
|
114
|
+
seen.add(key);
|
|
115
|
+
hits.push({ file, line: lineOf(text, index), target });
|
|
116
|
+
};
|
|
117
|
+
for (const pattern of [FROM_CLAUSE, DYNAMIC_IMPORT]) {
|
|
118
|
+
pattern.lastIndex = 0;
|
|
119
|
+
for (const match of text.matchAll(pattern)) consider(match.index ?? 0, match[2] ?? '');
|
|
120
|
+
}
|
|
121
|
+
NON_LITERAL_IMPORT.lastIndex = 0;
|
|
122
|
+
unseen += [...text.matchAll(NON_LITERAL_IMPORT)].length;
|
|
123
|
+
}
|
|
124
|
+
return { measured: true, hits, unseen };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Gate form: 1 on a hit, 0 when clean. An unmeasured check blocks only when a changed file is included. */
|
|
128
|
+
export async function runPublicExportImportGate(io: { args: readonly string[]; cwd: string; log: (line: string) => void; error: (line: string) => void }): Promise<number> {
|
|
129
|
+
const at = io.args.indexOf('--changed-files');
|
|
130
|
+
const files = at >= 0 ? io.args.slice(at + 1).filter((arg) => !arg.startsWith('--')) : [];
|
|
131
|
+
if (files.length === 0) return 0;
|
|
132
|
+
let result: ExportImportCheck;
|
|
133
|
+
try { result = await exportImportCheck(files, io.cwd); }
|
|
134
|
+
catch { result = { measured: false, hits: [], unseen: 0, detail: '검사 실행 실패' }; }
|
|
135
|
+
if (!result.measured) {
|
|
136
|
+
let publicFiles = files;
|
|
137
|
+
try {
|
|
138
|
+
const { loadExportConfig, selectExportFiles } = await exportSelectors();
|
|
139
|
+
publicFiles = selectExportFiles(files, loadExportConfig(io.cwd));
|
|
140
|
+
} catch { publicFiles = files; }
|
|
141
|
+
if (publicFiles.length === 0) { io.log('⚠ public-export-import: 못 쟀다 — 공개 대상 변경 없음'); return 0; }
|
|
142
|
+
io.error(`✗ public-export-import: 못 쟀다 — 막는다(${result.detail ?? '?'})`);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
if (result.unseen > 0) io.log(`⚠ public-export-import: 비리터럴 지정자 ${result.unseen}곳은 못 봄`);
|
|
146
|
+
if (result.hits.length === 0) return 0;
|
|
147
|
+
io.error(`✗ public-export-import: 공개 파일이 공개 내보내기에서 제외된 모듈을 import 한다 (${result.hits.length}곳) — 공개본 빌드가 여기서 깨진다:`);
|
|
148
|
+
for (const hit of result.hits) io.error(` ${hit.file}:${hit.line} → ${hit.target}`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (import.meta.main) {
|
|
153
|
+
process.exit(await runPublicExportImportGate({ args: process.argv.slice(2), cwd: resolve(import.meta.dir, '..'), log: console.log, error: console.error }));
|
|
154
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
detectTestInterference,
|
|
4
4
|
formatIsolatedFailureSummary,
|
|
5
|
+
runBunTest,
|
|
5
6
|
type InterferenceReport,
|
|
6
7
|
type TestRunner,
|
|
7
8
|
} from './detect-test-interference.js';
|
|
@@ -11,6 +12,8 @@ const TEST_FILE = /\.test\.tsx?$/;
|
|
|
11
12
|
|
|
12
13
|
export type TestInterferenceGateIo = {
|
|
13
14
|
args?: readonly string[];
|
|
15
|
+
/** Landing tree. `bun test` of the changed files runs here, not in process.cwd(). */
|
|
16
|
+
cwd?: string;
|
|
14
17
|
log?: (message: string) => void;
|
|
15
18
|
detect?: (files: readonly string[], runner?: TestRunner) => Promise<InterferenceReport>;
|
|
16
19
|
runner?: TestRunner;
|
|
@@ -36,6 +39,7 @@ export function selectedTestFiles(files: readonly string[]): string[] {
|
|
|
36
39
|
export async function runTestInterferenceGate(io: TestInterferenceGateIo = {}): Promise<number> {
|
|
37
40
|
const args = io.args ?? process.argv.slice(2);
|
|
38
41
|
const log = io.log ?? console.log;
|
|
42
|
+
const cwd = io.cwd ?? process.cwd();
|
|
39
43
|
const changed = parseChangedFiles(args);
|
|
40
44
|
const tests = selectedTestFiles(changed ?? []);
|
|
41
45
|
if (tests.length < 2) {
|
|
@@ -48,7 +52,7 @@ export async function runTestInterferenceGate(io: TestInterferenceGateIo = {}):
|
|
|
48
52
|
const capNotice = omitted > 0 ? ` · 상한 ${MAX_INSPECTED_TEST_FILES}개 적용, ${omitted}개 미검사.` : '';
|
|
49
53
|
let report: InterferenceReport;
|
|
50
54
|
try {
|
|
51
|
-
report = await (io.detect ?? detectTestInterference)(inspected, io.runner);
|
|
55
|
+
report = await (io.detect ?? detectTestInterference)(inspected, io.runner ?? ((files) => runBunTest(files, undefined, cwd)));
|
|
52
56
|
} catch {
|
|
53
57
|
log(`[test-interference-gate] 경고: 측정 불가 — 판정기 실행 실패, 간섭 없음으로 처리하지 않음.${capNotice}`);
|
|
54
58
|
return 0;
|
|
@@ -15,9 +15,18 @@ import {
|
|
|
15
15
|
import { debug } from '../src/debug/log.js';
|
|
16
16
|
import { tscEnv } from '../src/typecheck-ratchet.js';
|
|
17
17
|
|
|
18
|
+
/** Inherited GIT_DIR / GIT_WORK_TREE would make `--cwd <other tree>` still read the launcher's repo. */
|
|
19
|
+
function gitEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
20
|
+
const env = { ...base };
|
|
21
|
+
delete env.GIT_DIR;
|
|
22
|
+
delete env.GIT_WORK_TREE;
|
|
23
|
+
delete env.GIT_COMMON_DIR;
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/** git 조회용 — 명령 실패는 호출자가 빈 출력과 구분할 수 있게 전파한다. */
|
|
19
|
-
function sh(cmd: string): string {
|
|
20
|
-
return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
28
|
+
function sh(cmd: string, cwd = process.cwd()): string {
|
|
29
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd, env: gitEnv() });
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export type TscRun = { out: string; ran: boolean; why?: string };
|
|
@@ -59,13 +68,15 @@ export type ChangedTsFiles = {
|
|
|
59
68
|
failures?: { command: string; why: string }[];
|
|
60
69
|
};
|
|
61
70
|
|
|
62
|
-
/** 선택한 base와 파일 수를 함께 보존한다. 0개도 base 기준 관측값이다.
|
|
63
|
-
|
|
71
|
+
/** 선택한 base와 파일 수를 함께 보존한다. 0개도 base 기준 관측값이다.
|
|
72
|
+
* `cwd` 는 착지 트리다 — `pr land --cwd` 가 프로세스 cwd 가 아니라 그 트리를 재게 한다. */
|
|
73
|
+
export function changedTsFiles(run: (cmd: string) => string = sh, env: NodeJS.ProcessEnv = process.env, cwd = process.cwd(), baseRef?: string): ChangedTsFiles {
|
|
64
74
|
let base: string;
|
|
65
75
|
let baseSource: string;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
const pinned = baseRef ?? env.TSC_BASE_REF;
|
|
77
|
+
if (pinned) {
|
|
78
|
+
base = pinned;
|
|
79
|
+
baseSource = baseRef ? 'baseRef' : 'TSC_BASE_REF';
|
|
69
80
|
} else if (env.GITHUB_BASE_REF) {
|
|
70
81
|
base = `origin/${env.GITHUB_BASE_REF}`;
|
|
71
82
|
baseSource = 'GITHUB_BASE_REF';
|
|
@@ -108,8 +119,10 @@ export function changedTsFiles(run = sh, env: NodeJS.ProcessEnv = process.env):
|
|
|
108
119
|
if (!file) continue;
|
|
109
120
|
const ts = /\.tsx?$/.test(file);
|
|
110
121
|
// A deleted .ts is not a compile candidate, so it must not count as «should have been collected».
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
// existsSync is bound to the landing tree — process.cwd() would count the launcher's files.
|
|
123
|
+
const present = existsSync(join(cwd, file));
|
|
124
|
+
if (!ts || present) diffFiles.add(file);
|
|
125
|
+
if (ts && present) files.add(file);
|
|
113
126
|
}
|
|
114
127
|
} catch (cause) {
|
|
115
128
|
failures.push({ command, why: cause instanceof Error ? cause.message : String(cause) });
|
|
@@ -766,7 +779,7 @@ export function readMergeBaseDiagnostics(base: string, command: string, cwd = pr
|
|
|
766
779
|
const workspace = mkdtempSync(join(tmpdir(), 'ci-typecheck-baseline-'));
|
|
767
780
|
const worktree = join(workspace, 'base');
|
|
768
781
|
try {
|
|
769
|
-
execFileSync('git', ['worktree', 'add', '--detach', worktree, base], { cwd, stdio: 'pipe' });
|
|
782
|
+
execFileSync('git', ['worktree', 'add', '--detach', worktree, base], { cwd, stdio: 'pipe', env: gitEnv() });
|
|
770
783
|
const modules = join(cwd, 'node_modules');
|
|
771
784
|
if (!existsSync(modules)) return { unavailable: `기준 worktree 의존성을 재사용할 수 없다(${modules} 없음)` };
|
|
772
785
|
symlinkSync(modules, join(worktree, 'node_modules'));
|
|
@@ -780,7 +793,7 @@ export function readMergeBaseDiagnostics(base: string, command: string, cwd = pr
|
|
|
780
793
|
} catch (error) {
|
|
781
794
|
return { unavailable: error instanceof Error ? error.message : String(error) };
|
|
782
795
|
} finally {
|
|
783
|
-
try { execFileSync('git', ['worktree', 'remove', '--force', worktree], { cwd, stdio: 'pipe' }); }
|
|
796
|
+
try { execFileSync('git', ['worktree', 'remove', '--force', worktree], { cwd, stdio: 'pipe', env: gitEnv() }); }
|
|
784
797
|
catch { /* acquisition failure is rendered by the caller */ }
|
|
785
798
|
rmSync(workspace, { recursive: true, force: true });
|
|
786
799
|
}
|
|
@@ -792,6 +805,10 @@ export type GateIo = {
|
|
|
792
805
|
literalUnionMembers?: (changed: ReadonlySet<string>, base: string) => LiteralUnionMember[] | LiteralUnionMemberCheck;
|
|
793
806
|
readBaselineDiagnostics?: (base: string, command: string) => BaselineDiagnostics;
|
|
794
807
|
runTscCmd: (cmd: string) => TscRun;
|
|
808
|
+
/** Landing tree. Git, existsSync, tsc, and the test baseline are measured here — not in process.cwd(). */
|
|
809
|
+
cwd?: string;
|
|
810
|
+
/** Explicit base revision. Same rule as TSC_BASE_REF, without reading the process environment. */
|
|
811
|
+
baseRef?: string;
|
|
795
812
|
log: (message: string) => void;
|
|
796
813
|
warn: (message: string) => void;
|
|
797
814
|
error: (message: string) => void;
|
|
@@ -861,8 +878,10 @@ function inspectTscResult(tsc: TscRun, changed: ReadonlySet<string>, baseline: R
|
|
|
861
878
|
}
|
|
862
879
|
|
|
863
880
|
export function runGate(io: Partial<GateIo> = {}): void {
|
|
864
|
-
const
|
|
865
|
-
const
|
|
881
|
+
const cwd = io.cwd ?? process.cwd();
|
|
882
|
+
const defaultChanged = () => changedTsFiles((cmd) => sh(cmd, cwd), process.env, cwd, io.baseRef);
|
|
883
|
+
const { changedTsFiles: getChanged = defaultChanged, runTscCmd = (cmd: string) => runTsc(cmd, ((command, options) => execSync(command, { ...options, cwd, env: gitEnv() })) as typeof execSync), log = (message: string) => console.log(message), warn = (message: string) => console.warn(message), error = (message: string) => console.error(message), exit = ((code: number) => process.exit(code)) as GateIo['exit'] } = io;
|
|
884
|
+
const readBaselineDiagnostics = io.readBaselineDiagnostics ?? ((base: string, command: string) => readMergeBaseDiagnostics(base, command, cwd));
|
|
866
885
|
// Set 주입은 기존 테스트 심이며 기준 revision이 없다. 실제 ChangedTsFiles 경로만 Git 관측을 요구한다.
|
|
867
886
|
const requiredExportFields = io.requiredExportFields ?? ((changed: ReadonlySet<string>, base: string) => base === 'injected test seam'
|
|
868
887
|
? { fields: [], removedSymbols: [], parameterIncreases: [] }
|
|
@@ -966,8 +985,8 @@ export function runGate(io: Partial<GateIo> = {}): void {
|
|
|
966
985
|
}, { level: 'info' });
|
|
967
986
|
log(`[tsc-gate] root 설정 범위 ${rootScope.size}개 관측 (apps/pwa/** 제외).`);
|
|
968
987
|
if (runsPwa) log(`[tsc-gate] PWA 설정 범위 ${pwaScope.size}개 관측.`);
|
|
969
|
-
const tscBin = existsSync('node_modules/.bin/tsc') ? 'node_modules/.bin/tsc' : 'npx tsc';
|
|
970
|
-
const baseline = readTestTypecheckBaseline(
|
|
988
|
+
const tscBin = existsSync(join(cwd, 'node_modules/.bin/tsc')) ? 'node_modules/.bin/tsc' : 'npx tsc';
|
|
989
|
+
const baseline = readTestTypecheckBaseline(cwd);
|
|
971
990
|
const rootCommand = `${tscBin} --noEmit -p ${TYPECHECK_GATE_CONFIG}`;
|
|
972
991
|
const pwaCommand = `${tscBin} --noEmit -p apps/pwa/tsconfig.json`;
|
|
973
992
|
const pwaEnvCommand = `${tscBin} --noEmit -p tsconfig.pwa-env.json`;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// DOC-ROT① — 내부 매뉴얼(내부 문서 `*`)의 명령·저장소 경로를 실제와 대조한다.
|
|
3
|
+
// bun scripts/docs/doc-rot.ts [--json] [files…] (기본 = 내부 문서 `*`)
|
|
4
|
+
// 명령 판정은 scripts/docs-cli-check.ts 의 checkCommands 만 쓴다(같은 판정을 다시 짜지 않는다).
|
|
5
|
+
// 경로 판정은 인라인 코드 안의 src/ · scripts/ · docs/ · graphs/ · test/ 가 repoRoot 에 있는지다.
|
|
6
|
+
// ⛔ --help 를 못 잰 명령은 unmeasured 로 따로 센다 — «없다»로 세지 않는다.
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { join, relative, resolve } from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
checkCommands,
|
|
12
|
+
extractElanousCommands,
|
|
13
|
+
type Finding,
|
|
14
|
+
type HelpRunner,
|
|
15
|
+
} from '../docs-cli-check.js';
|
|
16
|
+
|
|
17
|
+
const REPO = resolve(import.meta.dir, '../..');
|
|
18
|
+
const PATH_ROOTS = ['src/', 'scripts/', 'docs/', 'graphs/', 'test/'] as const;
|
|
19
|
+
const PASSTHROUGH_COMMANDS = new Set(['gh', 'git']);
|
|
20
|
+
|
|
21
|
+
export interface MissingPath { line: number; path: string }
|
|
22
|
+
export interface DocRotFinding extends Omit<Finding, 'kind'> {
|
|
23
|
+
kind: Finding['kind'] | 'passthrough' | 'globalFlag' | 'intentional';
|
|
24
|
+
}
|
|
25
|
+
export interface DocRotFile {
|
|
26
|
+
file: string;
|
|
27
|
+
commands: DocRotFinding[];
|
|
28
|
+
missingPaths: MissingPath[];
|
|
29
|
+
}
|
|
30
|
+
export interface DocRotTotals {
|
|
31
|
+
files: number;
|
|
32
|
+
staleFiles: number;
|
|
33
|
+
missingPaths: number;
|
|
34
|
+
badCommands: number;
|
|
35
|
+
unmeasured: number;
|
|
36
|
+
passthrough: number;
|
|
37
|
+
globalFlag: number;
|
|
38
|
+
intentional: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DocRotReport {
|
|
41
|
+
files: DocRotFile[];
|
|
42
|
+
totals: DocRotTotals;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ScanDocRotInput {
|
|
46
|
+
files: readonly string[];
|
|
47
|
+
repoRoot: string;
|
|
48
|
+
help?: HelpRunner;
|
|
49
|
+
read?: (file: string) => string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 인라인 코드에서 저장소 경로를 뽑는다. 줄번호 꼬리(`:12`)는 떼고, glob(`*`)은 건너뛴다. */
|
|
53
|
+
export function extractRepoPaths(text: string): MissingPath[] {
|
|
54
|
+
const out: MissingPath[] = [];
|
|
55
|
+
const seen = new Set<string>();
|
|
56
|
+
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
57
|
+
const raw = m[1]!;
|
|
58
|
+
if (raw.includes('*')) continue;
|
|
59
|
+
if (!PATH_ROOTS.some((root) => raw.startsWith(root))) continue;
|
|
60
|
+
const path = raw.replace(/:\d+$/, '');
|
|
61
|
+
if (!PATH_ROOTS.some((root) => path.startsWith(root)) || path.endsWith('/')) continue;
|
|
62
|
+
const line = text.slice(0, m.index ?? 0).split('\n').length;
|
|
63
|
+
const key = `${line}\0${path}`;
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
out.push({ line, path });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 파일마다 명령 findings(checkCommands 재사용)와 없는 경로를 센다. */
|
|
72
|
+
export function scanDocRot(input: ScanDocRotInput): DocRotReport {
|
|
73
|
+
const read = input.read ?? ((file: string) => readFileSync(file, 'utf8'));
|
|
74
|
+
const texts = input.files.map((file) => ({ file, text: read(file) }));
|
|
75
|
+
const help: HelpRunner = input.help ?? ((args) => {
|
|
76
|
+
const result = spawnSync('bun', ['bin/elanous.mjs', '--test', ...args, '--help'], {
|
|
77
|
+
cwd: input.repoRoot, encoding: 'utf8', timeout: 60_000, env: { ...process.env, NO_COLOR: '1' },
|
|
78
|
+
});
|
|
79
|
+
return { ok: result.status === 0, out: `${result.stdout ?? ''}\n${result.stderr ?? ''}` };
|
|
80
|
+
});
|
|
81
|
+
let commandRoot: ReturnType<HelpRunner> | undefined;
|
|
82
|
+
const cachedHelp: HelpRunner = (args) => {
|
|
83
|
+
if (args.length === 0) return commandRoot ??= help(args);
|
|
84
|
+
return help(args);
|
|
85
|
+
};
|
|
86
|
+
const checked = texts.map(({ file, text }) => {
|
|
87
|
+
const label = relative(input.repoRoot, file) || file;
|
|
88
|
+
return { file: label, text, commands: checkCommands(extractElanousCommands(label, text), cachedHelp) };
|
|
89
|
+
});
|
|
90
|
+
const hasUnknownFlag = checked.some(({ commands }) => commands.some((finding) => finding.kind === 'unknown-flag'));
|
|
91
|
+
// A separate lookup measures root Options; a failure must not mean "no global flags".
|
|
92
|
+
const rootOptionsHelp = hasUnknownFlag ? help([]) : commandRoot;
|
|
93
|
+
const rootOptions = rootOptionsHelp?.ok ? (rootOptionsHelp.out.split(/\nOptions:\s*\n/)[1] ?? '').split(/\n\S[^\n]*:\s*\n/)[0]! : '';
|
|
94
|
+
const globalFlags = new Set([...rootOptions.matchAll(/^\s+(?:-\w,\s*)?(--[a-z][a-z0-9-]*)\b/gm)].map((match) => match[1]!));
|
|
95
|
+
const files: DocRotFile[] = checked.map(({ file, text, commands }) => {
|
|
96
|
+
const lines = text.split('\n');
|
|
97
|
+
const classified: DocRotFinding[] = commands.map((finding) => {
|
|
98
|
+
if (finding.kind !== 'unmeasured' && (lines[finding.ref.line - 1] ?? '').includes('⛔')) return { ...finding, kind: 'intentional' };
|
|
99
|
+
if (finding.kind !== 'unknown-flag') return finding;
|
|
100
|
+
if (PASSTHROUGH_COMMANDS.has(finding.ref.cmd)) return { ...finding, kind: 'passthrough' };
|
|
101
|
+
if (!rootOptionsHelp?.ok) return { ...finding, kind: 'unmeasured' };
|
|
102
|
+
const flag = /^--[a-z][a-z0-9-]*/.exec(finding.detail)?.[0];
|
|
103
|
+
if (flag && globalFlags.has(flag)) return { ...finding, kind: 'globalFlag' };
|
|
104
|
+
return finding;
|
|
105
|
+
});
|
|
106
|
+
const missingPaths = extractRepoPaths(text).filter((entry) => !existsSync(join(input.repoRoot, entry.path)));
|
|
107
|
+
return { file, commands: classified, missingPaths };
|
|
108
|
+
});
|
|
109
|
+
const isBad = (finding: DocRotFinding) => finding.kind === 'unknown-command' || finding.kind === 'unknown-subcommand' || finding.kind === 'unknown-flag';
|
|
110
|
+
const count = (kind: DocRotFinding['kind']) => files.reduce((sum, file) => sum + file.commands.filter((finding) => finding.kind === kind).length, 0);
|
|
111
|
+
const totals: DocRotTotals = {
|
|
112
|
+
files: files.length,
|
|
113
|
+
staleFiles: files.filter((file) => file.missingPaths.length > 0 || file.commands.some(isBad)).length,
|
|
114
|
+
missingPaths: files.reduce((sum, file) => sum + file.missingPaths.length, 0),
|
|
115
|
+
badCommands: files.reduce((sum, file) => sum + file.commands.filter(isBad).length, 0),
|
|
116
|
+
unmeasured: count('unmeasured'),
|
|
117
|
+
passthrough: count('passthrough'),
|
|
118
|
+
globalFlag: count('globalFlag'),
|
|
119
|
+
intentional: count('intentional'),
|
|
120
|
+
};
|
|
121
|
+
return { files, totals };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function defaultManualFiles(repoRoot: string = REPO): string[] {
|
|
125
|
+
return readdirSync(join(repoRoot, 'docs', 'manual'))
|
|
126
|
+
.filter((name) => name.endsWith('.md'))
|
|
127
|
+
.sort()
|
|
128
|
+
.map((name) => join(repoRoot, 'docs', 'manual', name));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderTable(report: DocRotReport): string {
|
|
132
|
+
const lines = [
|
|
133
|
+
`doc-rot — 매뉴얼 ${report.totals.files} · 낡은 파일 ${report.totals.staleFiles} · 없는 경로 ${report.totals.missingPaths} · 깨진 명령 ${report.totals.badCommands} · 못 잰 명령 ${report.totals.unmeasured} · 통과 래퍼 ${report.totals.passthrough} · 전역 옵션 ${report.totals.globalFlag} · 의도적 예시 ${report.totals.intentional}`,
|
|
134
|
+
];
|
|
135
|
+
for (const file of report.files) {
|
|
136
|
+
for (const finding of file.commands) lines.push(` ${finding.kind} ${file.file}:${finding.ref.line} ${finding.detail}`);
|
|
137
|
+
for (const missing of file.missingPaths) lines.push(` missing-path ${file.file}:${missing.line} ${missing.path}`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (import.meta.main) {
|
|
143
|
+
try {
|
|
144
|
+
const { registerStandaloneLogSink } = await import('../../src/domains/standalone-log-sink.js');
|
|
145
|
+
await registerStandaloneLogSink('doc-rot');
|
|
146
|
+
} catch { /* fail-open — 관측 배선 실패가 측정을 막지 않는다 */ }
|
|
147
|
+
const { debug } = await import('../../src/debug/log.js');
|
|
148
|
+
const json = process.argv.includes('--json');
|
|
149
|
+
const args = process.argv.slice(2).filter((arg) => arg !== '--json');
|
|
150
|
+
const files = args.length ? args.map((arg) => resolve(arg)) : defaultManualFiles();
|
|
151
|
+
const report = scanDocRot({ files, repoRoot: REPO });
|
|
152
|
+
debug.log('docs.rot', 'scanned', {
|
|
153
|
+
files: report.totals.files,
|
|
154
|
+
staleFiles: report.totals.staleFiles,
|
|
155
|
+
missingPaths: report.totals.missingPaths,
|
|
156
|
+
badCommands: report.totals.badCommands,
|
|
157
|
+
unmeasured: report.totals.unmeasured,
|
|
158
|
+
passthrough: report.totals.passthrough,
|
|
159
|
+
globalFlag: report.totals.globalFlag,
|
|
160
|
+
intentional: report.totals.intentional,
|
|
161
|
+
});
|
|
162
|
+
console.log(json ? JSON.stringify(report) : renderTable(report));
|
|
163
|
+
process.exit(report.totals.badCommands + report.totals.missingPaths > 0 ? 1 : 0);
|
|
164
|
+
}
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* 노드 종류 = src/graph-kinds/registry.ts (HARNESS_CORE_KINDS · WORKFLOW_CORE_KINDS · listNodeKinds)
|
|
7
7
|
* 그래프 = graphs/ YAML 을 src/self-implement/graph-yaml.ts 의 loadGraphTemplates · edgeMapOf 가 읽는다
|
|
8
8
|
*
|
|
9
|
-
* renderGraphAnatomy 는 순수하다 — 파일을 쓰지 않고 process.cwd() 를 읽지 않는다.
|
|
10
|
-
* 뿌리·커밋·시각은 인자로만 받는다. main 만
|
|
9
|
+
* renderGraphAnatomy · diffAnatomy 는 순수하다 — 파일을 쓰지 않고 process.cwd() 를 읽지 않는다.
|
|
10
|
+
* 뿌리·커밋·시각은 인자로만 받는다. 쓰기는 main 만 한다.
|
|
11
|
+
* main: `--out <path>` (기본 내부 문서 `graph-anatomy`) 와 같은 이름 `.json` 옆 파일.
|
|
12
|
+
* git 트리가 아니면 죽지 않고 `commit: (설치본 v<package.json version>)`.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { execFileSync } from 'node:child_process';
|
|
@@ -35,6 +37,22 @@ export interface RenderGraphAnatomyInput {
|
|
|
35
37
|
readonly generatedAt: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** `<out>.json` 옆 파일. graphs 값은 graph_id → nodeId 목록. */
|
|
41
|
+
export interface AnatomySnapshot {
|
|
42
|
+
readonly generatedAt: string;
|
|
43
|
+
readonly commit: string;
|
|
44
|
+
readonly graphs: Readonly<Record<string, readonly string[]>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AnatomyDiff {
|
|
48
|
+
readonly addedGraphs: readonly string[];
|
|
49
|
+
readonly removedGraphs: readonly string[];
|
|
50
|
+
/** `graph_id/nodeId` */
|
|
51
|
+
readonly addedNodes: readonly string[];
|
|
52
|
+
/** `graph_id/nodeId` */
|
|
53
|
+
readonly removedNodes: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
export interface ScannedGraphDir {
|
|
39
57
|
/** graphsRoot 기준 상대 경로. 뿌리 자신은 '.' */
|
|
40
58
|
readonly dir: string;
|
|
@@ -165,6 +183,58 @@ function graphIdPaths(graphsRoot: string, dir: string): ReadonlyMap<string, stri
|
|
|
165
183
|
return paths;
|
|
166
184
|
}
|
|
167
185
|
|
|
186
|
+
function sortedIds(ids: Iterable<string>): string[] {
|
|
187
|
+
return [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 순수. 어제 판과 오늘 판의 그래프·노드 차이. 파일을 읽거나 쓰지 않는다. */
|
|
191
|
+
export function diffAnatomy(prev: AnatomySnapshot, next: AnatomySnapshot): AnatomyDiff {
|
|
192
|
+
const prevIds = new Set(Object.keys(prev.graphs));
|
|
193
|
+
const nextIds = new Set(Object.keys(next.graphs));
|
|
194
|
+
const addedGraphs = sortedIds([...nextIds].filter((id) => !prevIds.has(id)));
|
|
195
|
+
const removedGraphs = sortedIds([...prevIds].filter((id) => !nextIds.has(id)));
|
|
196
|
+
const addedNodes: string[] = [];
|
|
197
|
+
const removedNodes: string[] = [];
|
|
198
|
+
// A graph that appears or disappears contributes all of its nodes, so the node list never hides them (ACP must-fix).
|
|
199
|
+
for (const id of sortedIds([...new Set([...prevIds, ...nextIds])])) {
|
|
200
|
+
const before = new Set(prev.graphs[id] ?? []);
|
|
201
|
+
const after = new Set(next.graphs[id] ?? []);
|
|
202
|
+
for (const nodeId of sortedIds([...after].filter((node) => !before.has(node)))) addedNodes.push(`${id}/${nodeId}`);
|
|
203
|
+
for (const nodeId of sortedIds([...before].filter((node) => !after.has(node)))) removedNodes.push(`${id}/${nodeId}`);
|
|
204
|
+
}
|
|
205
|
+
return { addedGraphs, removedGraphs, addedNodes, removedNodes };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 머리(`# Graph anatomy` 와 생성물 경고) 바로 아래. 파일을 쓰지 않는다. */
|
|
209
|
+
export function renderYesterdaySection(prev: AnatomySnapshot | null, next: AnatomySnapshot): string {
|
|
210
|
+
if (prev === null) return ['## 어제 대비', '', '이전 판 없음(첫 생성)', ''].join('\n');
|
|
211
|
+
const diff = diffAnatomy(prev, next);
|
|
212
|
+
const lines = ['## 어제 대비', ''];
|
|
213
|
+
const changed = diff.addedGraphs.length + diff.removedGraphs.length + diff.addedNodes.length + diff.removedNodes.length;
|
|
214
|
+
if (changed === 0) {
|
|
215
|
+
lines.push('변화 없음', '');
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
for (const id of diff.addedGraphs) lines.push(`+ ${id}`);
|
|
219
|
+
for (const id of diff.removedGraphs) lines.push(`- ${id}`);
|
|
220
|
+
for (const node of diff.addedNodes) lines.push(`+ ${node}`);
|
|
221
|
+
for (const node of diff.removedNodes) lines.push(`- ${node}`);
|
|
222
|
+
lines.push('');
|
|
223
|
+
return lines.join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function snapshotOf(scanned: readonly ScannedGraphDir[], commit: string, generatedAt: string): AnatomySnapshot {
|
|
227
|
+
const graphs: Record<string, string[]> = {};
|
|
228
|
+
for (const { result } of scanned) {
|
|
229
|
+
for (const template of Object.values(result.templates)) {
|
|
230
|
+
const ids = template.nodes.map((node) => node.nodeId);
|
|
231
|
+
const prev = graphs[template.graphId];
|
|
232
|
+
graphs[template.graphId] = prev === undefined ? ids : [...prev, ...ids.filter((id) => !prev.includes(id))];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { generatedAt, commit, graphs };
|
|
236
|
+
}
|
|
237
|
+
|
|
168
238
|
function kindRows(entries: readonly NodeKindEntry[]): string[] {
|
|
169
239
|
return [...entries]
|
|
170
240
|
.sort((a, b) => (a.graph < b.graph ? -1 : a.graph > b.graph ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0))
|
|
@@ -179,7 +249,7 @@ function issueLines(label: string, issues: readonly GraphParseIssue[]): string[]
|
|
|
179
249
|
/**
|
|
180
250
|
* 마크다운 한 장. 파일을 쓰지 않는다. graphsRoot · commit · generatedAt 만 읽는다.
|
|
181
251
|
*/
|
|
182
|
-
export function renderGraphAnatomy(input: RenderGraphAnatomyInput): string {
|
|
252
|
+
export function renderGraphAnatomy(input: RenderGraphAnatomyInput, yesterday?: string): string {
|
|
183
253
|
const scanned = scanGraphDirs(input.graphsRoot);
|
|
184
254
|
const templates: { dir: string; fileLabel: string; template: GraphTemplateSpec }[] = [];
|
|
185
255
|
for (const { dir, result } of scanned) {
|
|
@@ -207,6 +277,7 @@ export function renderGraphAnatomy(input: RenderGraphAnatomyInput): string {
|
|
|
207
277
|
`- scannedDirs: ${scanned.length}`,
|
|
208
278
|
`- templates: ${templates.length}`,
|
|
209
279
|
'',
|
|
280
|
+
...(yesterday === undefined ? [] : [yesterday.trimEnd(), '']),
|
|
210
281
|
'## 표 1 노드 종류',
|
|
211
282
|
'',
|
|
212
283
|
'원천: `src/graph-kinds/registry.ts` — `HARNESS_CORE_KINDS` · `WORKFLOW_CORE_KINDS` · `listNodeKinds()`.',
|
|
@@ -292,23 +363,74 @@ function repoRootFromHere(): string {
|
|
|
292
363
|
return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
293
364
|
}
|
|
294
365
|
|
|
295
|
-
|
|
296
|
-
|
|
366
|
+
/** git 트리가 아니면 죽지 않는다. 설치본은 package.json version 으로 적는다. */
|
|
367
|
+
export function readCommit(repoRoot: string): string {
|
|
368
|
+
try {
|
|
369
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
370
|
+
} catch {
|
|
371
|
+
let version = 'unknown';
|
|
372
|
+
try {
|
|
373
|
+
const raw = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8')) as { version?: unknown };
|
|
374
|
+
if (typeof raw.version === 'string' && raw.version.length > 0) version = raw.version;
|
|
375
|
+
} catch { /* 설치본인데 package.json 도 없으면 unknown */ }
|
|
376
|
+
return `(설치본 v${version})`;
|
|
377
|
+
}
|
|
297
378
|
}
|
|
298
379
|
|
|
299
380
|
export function defaultOutputPath(repoRoot: string): string {
|
|
300
381
|
return join(repoRoot, 'docs', 'generated', 'graph-anatomy.md');
|
|
301
382
|
}
|
|
302
383
|
|
|
384
|
+
/** 옆 파일 이름. `a.md` → `a.md.json`. */
|
|
385
|
+
export function snapshotPathOf(out: string): string {
|
|
386
|
+
return `${out}.json`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isSnapshot(value: unknown): value is AnatomySnapshot {
|
|
390
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
391
|
+
const record = value as Record<string, unknown>;
|
|
392
|
+
if (typeof record.generatedAt !== 'string' || typeof record.commit !== 'string') return false;
|
|
393
|
+
if (!record.graphs || typeof record.graphs !== 'object' || Array.isArray(record.graphs)) return false;
|
|
394
|
+
return Object.values(record.graphs as Record<string, unknown>).every((nodes) => Array.isArray(nodes) && nodes.every((node) => typeof node === 'string'));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** 쓰기 전의 옆 파일. 없거나 깨졌으면 null(첫 생성). 파일을 쓰지 않는다. */
|
|
398
|
+
export function readPreviousSnapshot(out: string): AnatomySnapshot | null {
|
|
399
|
+
const path = snapshotPathOf(out);
|
|
400
|
+
if (!existsSync(path)) return null;
|
|
401
|
+
try {
|
|
402
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
403
|
+
return isSnapshot(parsed) ? parsed : null;
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseFlag(argv: readonly string[], name: string): string | undefined {
|
|
410
|
+
const index = argv.indexOf(name);
|
|
411
|
+
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
412
|
+
if (index >= 0 && (value === undefined || value.startsWith('--'))) throw new Error(`${name} 에는 경로가 필요하다`);
|
|
413
|
+
return value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function parseOutArg(argv: readonly string[], fallback: string): string {
|
|
417
|
+
return parseFlag(argv, '--out') ?? fallback;
|
|
418
|
+
}
|
|
419
|
+
|
|
303
420
|
function main(): void {
|
|
304
421
|
const repoRoot = repoRootFromHere();
|
|
305
|
-
const
|
|
422
|
+
const argv = process.argv.slice(2);
|
|
423
|
+
const graphsRoot = parseFlag(argv, '--graphs') ?? join(repoRoot, 'graphs');
|
|
306
424
|
const generatedAt = new Date().toISOString();
|
|
307
|
-
const commit = readCommit(repoRoot);
|
|
308
|
-
const
|
|
309
|
-
const
|
|
425
|
+
const commit = readCommit(parseFlag(argv, '--repo') ?? repoRoot);
|
|
426
|
+
const out = parseOutArg(argv, defaultOutputPath(repoRoot));
|
|
427
|
+
const scanned = scanGraphDirs(graphsRoot);
|
|
428
|
+
const next = snapshotOf(scanned, commit, generatedAt);
|
|
429
|
+
const prev = readPreviousSnapshot(out);
|
|
430
|
+
const markdown = renderGraphAnatomy({ graphsRoot, commit, generatedAt }, renderYesterdaySection(prev, next));
|
|
310
431
|
mkdirSync(dirname(out), { recursive: true });
|
|
311
432
|
writeFileSync(out, markdown);
|
|
433
|
+
writeFileSync(snapshotPathOf(out), `${JSON.stringify(next, null, 2)}\n`);
|
|
312
434
|
process.stdout.write(`${out}\n`);
|
|
313
435
|
}
|
|
314
436
|
|