elanous 0.2.11 → 0.2.12
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/1147-466ec15afacc1a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-ee3795b112907122.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-0259994816604c49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-cefe8169e36cf62a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-cb95a7432c9ee744.js → page-d2e35228bc497119.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12b6d2f8d2d1aea.js → page-5c05b0da4bc1392c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b9efe868bfd36255.js → page-e0ccdb576812b523.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-813c4a52c9d05ca8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-a022b257950f39e6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-a2ffc717e04b8b0a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-b59ee4e70fc18fcf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-d5dc7ff69a37b4cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-2a9932ddb62d76bd.js → page-1d6a533ce5a6efcb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-dfca9b7d2de0b135.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-793fa536b3a6ef37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6dd3bfbbde2a5c2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-e7a529e0507ac86c.js → not-found-490653e8d4cc8852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-77447356baad75d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-6d604227e1664b71.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-0be8d4b2ced75ba0.js → page-4048543b4b7ad5e0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-4c3a3126abed551a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-64b0aecb1ca75e75.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-53f80edc6b8dc07c.js → page-db14d0be7f1c460c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-9547b5cec7cf76bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-2cddeb9e2aeedc55.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-e297866051595d81.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-baba2e047872fb0f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-e895da4ec9cac345.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/package.json +1 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +8 -2
- package/scripts/release-loop/ops-upgrade-node.ts +47 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/src/agent-mission/driver.ts +138 -16
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/chat/index.ts +1 -0
- package/src/cli/persona-cli.ts +17 -14
- package/src/cli/pod-cli.ts +17 -0
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/release-cli.ts +46 -6
- package/src/context-bus/context-now-surfaces.ts +17 -0
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +5 -2
- package/src/graph-runner/runner.ts +12 -8
- package/src/harness/harness-cli-command.ts +138 -16
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/receiver.ts +78 -1
- package/src/index.ts +33 -9
- package/src/intake-plane/discord-seat-work.ts +28 -2
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/nexus/api/http-server.ts +31 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +95 -53
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/presets.ts +8 -1
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +48 -0
- package/src/seat-loop/seat-loop.ts +168 -7
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +8 -0
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-project-command.ts +78 -4
- package/src/telegram.ts +32 -5
- package/src/user-config.ts +8 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → -6XrAoD3eU7NyMHAE4xPS}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → -6XrAoD3eU7NyMHAE4xPS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Seven-day read-only measurement of self-implement Pod memory by goal kind. */
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { LogStore, logsDbPath, type LogStoreRow } from '../src/mss/logging/log-store.js';
|
|
5
|
+
|
|
6
|
+
export const GOAL_TYPES = ['test', 'code', 'pwa-build', 'docs'] as const;
|
|
7
|
+
export type GoalType = typeof GOAL_TYPES[number];
|
|
8
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const MIB = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
type Observation = Pick<LogStoreRow, 'ts_ms' | 'event' | 'data' | 'category'>;
|
|
12
|
+
type Run = { type: GoalType | null; limit: string | null; peakMiB: number | null; samples: number; terminated: boolean; oomKilled: boolean };
|
|
13
|
+
type MemorySample = { cgroupBytes?: unknown };
|
|
14
|
+
export type MemoryMeasurement = {
|
|
15
|
+
window: { since: string; until: string };
|
|
16
|
+
sources: { logs: string; observations: number; samples: number; unclassifiedRuns: number; classification: 'goal-fields-or-space-id'; peakCoverage: 'recorded-samples-only' };
|
|
17
|
+
byGoalType: Array<{ goalType: GoalType; runs: number; measured: number; missingPeak: number; samples: number; memoryLimits: Record<string, number>; terminations: number; oomKilled: number; peakMiB: { p50: number | null; p95: number | null; max: number | null } }>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function payload(raw: string | null): Record<string, unknown> {
|
|
21
|
+
try {
|
|
22
|
+
const parsed: unknown = JSON.parse(raw ?? 'null');
|
|
23
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
24
|
+
} catch { return {}; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Infer only unambiguous kinds from free text; never turn an unknown title into code. */
|
|
28
|
+
export function goalTypeOf(description: string): GoalType | null {
|
|
29
|
+
const kinds: GoalType[] = [];
|
|
30
|
+
if (/\b(?:pwa|next\.js|frontend|front-end|web app)[-\s]+(?:build|compile|typecheck|bundle)\b/i.test(description)) kinds.push('pwa-build');
|
|
31
|
+
if (/\b(?:test|tests|testing|spec|coverage|gate)\b/i.test(description) || /시험|테스트|검증/.test(description)) kinds.push('test');
|
|
32
|
+
if (/\b(?:docs?|documentation|readme|manual|report)\b/i.test(description) || /문서|보고서/.test(description)) kinds.push('docs');
|
|
33
|
+
if (/\b(?:implement|implementation|coding|code|cli)\b/i.test(description) || /구현|코드/.test(description)) kinds.push('code');
|
|
34
|
+
return kinds.length === 1 ? kinds[0]! : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function recordedGoalType(data: Record<string, unknown>, spaceId: string): GoalType | null {
|
|
38
|
+
if ('goalType' in data) return GOAL_TYPES.find((type) => type === data.goalType) ?? null;
|
|
39
|
+
if ('goalKind' in data) return GOAL_TYPES.find((type) => type === data.goalKind) ?? null;
|
|
40
|
+
const descriptions = [data.goalTitle, data.goalDescription, data.feature].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
|
41
|
+
return descriptions.length ? goalTypeOf(descriptions.join(' ')) : goalTypeOf(spaceId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function percentile(sorted: readonly number[], fraction: number): number | null {
|
|
45
|
+
if (!sorted.length) return null;
|
|
46
|
+
const index = (sorted.length - 1) * fraction;
|
|
47
|
+
const low = Math.floor(index);
|
|
48
|
+
return Math.round((sorted[low]! + (sorted[Math.ceil(index)]! - sorted[low]!) * (index - low)) * 100) / 100;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** All rows are read before aggregation: log query's default 100-row limit is not a seven-day census. */
|
|
52
|
+
export function measurePodMemoryByGoal(rows: readonly Observation[], nowMs: number = Date.now(), logs = logsDbPath()): MemoryMeasurement {
|
|
53
|
+
const sinceMs = nowMs - WEEK_MS;
|
|
54
|
+
const windowRows = rows.filter((row) => row.ts_ms >= sinceMs && row.ts_ms <= nowMs && row.category === 'self-implement.pod');
|
|
55
|
+
const runs = new Map<string, Run>();
|
|
56
|
+
const jobToSpace = new Map<string, string>();
|
|
57
|
+
let samples = 0;
|
|
58
|
+
const ordered = [...windowRows].sort((a, b) => a.ts_ms - b.ts_ms);
|
|
59
|
+
for (const row of ordered) {
|
|
60
|
+
if (row.event !== 'memory-limit') continue;
|
|
61
|
+
const data = payload(row.data);
|
|
62
|
+
const id = typeof data.spaceId === 'string' ? data.spaceId : '';
|
|
63
|
+
if (!id) continue;
|
|
64
|
+
runs.set(id, { type: recordedGoalType(data, id), limit: typeof data.memoryLimit === 'string' ? data.memoryLimit : null,
|
|
65
|
+
peakMiB: null, samples: 0, terminated: false, oomKilled: false });
|
|
66
|
+
}
|
|
67
|
+
for (const row of ordered) {
|
|
68
|
+
const data = payload(row.data);
|
|
69
|
+
const job = typeof data.job === 'string' ? data.job : '';
|
|
70
|
+
const directId = typeof data.spaceId === 'string' ? data.spaceId : '';
|
|
71
|
+
if (row.event === 'job-applied' && job && runs.has(directId)) jobToSpace.set(job, directId);
|
|
72
|
+
const run = runs.get(directId || jobToSpace.get(job) || '');
|
|
73
|
+
if (!run) continue;
|
|
74
|
+
if (row.event === 'job-finished') {
|
|
75
|
+
run.terminated = true;
|
|
76
|
+
run.oomKilled ||= data.containerReason === 'OOMKilled';
|
|
77
|
+
}
|
|
78
|
+
if (row.event === 'oom-evidence') { run.terminated = true; run.oomKilled = true; }
|
|
79
|
+
if (data.reason === 'OOMKilled') { run.terminated = true; run.oomKilled = true; }
|
|
80
|
+
if (row.event !== 'oom-evidence' && row.event !== 'memory-last') continue;
|
|
81
|
+
const rawSamples: unknown[] = row.event === 'oom-evidence' && Array.isArray(data.samples) ? data.samples : [data.sample];
|
|
82
|
+
for (const raw of rawSamples) {
|
|
83
|
+
const record = raw && typeof raw === 'object' ? raw as MemorySample : {};
|
|
84
|
+
const bytes = record.cgroupBytes;
|
|
85
|
+
if (typeof bytes !== 'number' || !Number.isSafeInteger(bytes) || bytes < 0) continue;
|
|
86
|
+
const reading = bytes / MIB;
|
|
87
|
+
run.peakMiB = Math.max(run.peakMiB ?? 0, reading);
|
|
88
|
+
run.samples++;
|
|
89
|
+
samples++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
window: { since: new Date(sinceMs).toISOString(), until: new Date(nowMs).toISOString() },
|
|
94
|
+
sources: { logs, observations: windowRows.length, samples, unclassifiedRuns: [...runs.values()].filter((run) => run.type === null).length,
|
|
95
|
+
classification: 'goal-fields-or-space-id', peakCoverage: 'recorded-samples-only' },
|
|
96
|
+
byGoalType: GOAL_TYPES.map((goalType) => {
|
|
97
|
+
const group = [...runs.values()].filter((run) => run.type === goalType);
|
|
98
|
+
const peaks = group.flatMap((run) => run.peakMiB === null ? [] : [run.peakMiB]).sort((a, b) => a - b);
|
|
99
|
+
const memoryLimits: Record<string, number> = {};
|
|
100
|
+
for (const run of group) if (run.limit) memoryLimits[run.limit] = (memoryLimits[run.limit] ?? 0) + 1;
|
|
101
|
+
return { goalType, runs: group.length, measured: peaks.length, missingPeak: group.length - peaks.length,
|
|
102
|
+
samples: group.reduce((count, run) => count + run.samples, 0), memoryLimits,
|
|
103
|
+
terminations: group.filter((run) => run.terminated).length, oomKilled: group.filter((run) => run.oomKilled).length,
|
|
104
|
+
peakMiB: { p50: percentile(peaks, 0.5), p95: percentile(peaks, 0.95), max: peaks.at(-1) ?? null } };
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function readPodMemoryMeasurement(path: string = logsDbPath(), nowMs: number = Date.now()): MemoryMeasurement {
|
|
110
|
+
if (!existsSync(path)) throw new Error(`log store not found: ${path}`);
|
|
111
|
+
const store = LogStore.openReadOnly(path);
|
|
112
|
+
try {
|
|
113
|
+
return measurePodMemoryByGoal(store.queryAll({ exactCategories: ['self-implement.pod'], sinceMs: nowMs - WEEK_MS, untilMs: nowMs }), nowMs, path);
|
|
114
|
+
} finally { store.close(); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (import.meta.main) {
|
|
118
|
+
const args = process.argv.slice(2);
|
|
119
|
+
const usage = 'Usage: bun scripts/measure-pod-memory-by-goal.ts [--json] [--logs-db <existing-logs.db>] [--help]';
|
|
120
|
+
const dbArg = args.indexOf('--logs-db');
|
|
121
|
+
const db = dbArg === -1 ? logsDbPath() : args[dbArg + 1];
|
|
122
|
+
if (!db || (dbArg !== -1 && (db.startsWith('--') || args.lastIndexOf('--logs-db') !== dbArg))
|
|
123
|
+
|| args.some((arg, index) => !['--json', '--help', '--logs-db'].includes(arg) && index !== dbArg + 1)) throw new Error(usage);
|
|
124
|
+
if (args.includes('--help')) console.log(usage);
|
|
125
|
+
else {
|
|
126
|
+
const result = readPodMemoryMeasurement(db);
|
|
127
|
+
if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
|
|
128
|
+
else for (const group of result.byGoalType) console.log(`${group.goalType}: runs=${group.runs} measured=${group.measured} peak p50=${group.peakMiB.p50 ?? 'n/a'} MiB p95=${group.peakMiB.p95 ?? 'n/a'} MiB OOMKilled=${group.oomKilled}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -15,7 +15,7 @@ const outcomeMetric = (name: string, node: Record<string, unknown> | undefined):
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
/** Rows shown in auto-approval.md but never counted toward the 8 blocking metrics. */
|
|
18
|
-
const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
|
|
18
|
+
export const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
|
|
19
19
|
|
|
20
20
|
/** `ok` · `warn` (a failure) · `unmeasured` (error or no output) — the macOS smoke never blocks publishing. */
|
|
21
21
|
function macSmokeLevel(node: Record<string, unknown> | undefined): 'ok' | 'warn' | 'unmeasured' {
|
|
@@ -11,10 +11,11 @@ export function runChecklistGate(context: GraphContext = readGraphContext()) {
|
|
|
11
11
|
if (gate.ok && gate.moved.length) {
|
|
12
12
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
13
13
|
const next = `${major}.${minor}.${patch! + 1}`;
|
|
14
|
+
// 이월은 «같은 칸»을 다음 판으로 옮기는 것 — 다른 판 id 충돌 검사(CKID #23407)의 대상이 아니다.
|
|
14
15
|
for (const id of gate.moved) {
|
|
15
16
|
const item = data.items.find((candidate) => candidate.id === id)!;
|
|
16
17
|
if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
|
|
17
|
-
try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }); }
|
|
18
|
+
try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }, { allowDuplicateId: true }); }
|
|
18
19
|
catch (error) {
|
|
19
20
|
if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
|
|
20
21
|
}
|
|
@@ -124,7 +124,8 @@ export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-20
|
|
|
124
124
|
/** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
|
|
125
125
|
export const POD_SHARD_FILE_CAP = 27;
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
// review-model-ab: Pod 에서 출력 없이 멈춤 2/2(10-04 0.2.11 · 로컬 단독 4/0 · 1.7s) — GATE-STALL 수리 전까지 Pod 밖.
|
|
128
|
+
export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts', 'scripts/review-model-ab.test.ts'];
|
|
128
129
|
/** Nightly-only whole-repository cases; gate runs the fixture cases in these same files. */
|
|
129
130
|
export const GATE_NIGHTLY_AUDITS: readonly string[] = [
|
|
130
131
|
// Replaces F12's real-repo bucket B apps/pwa scope check and its whole-repo export partition check.
|
|
@@ -267,7 +268,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
267
268
|
}, 0);
|
|
268
269
|
const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
|
|
269
270
|
const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
|
|
270
|
-
const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
271
|
+
const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
271
272
|
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
272
273
|
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
273
274
|
// 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
|
|
@@ -354,6 +355,11 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
357
|
}
|
|
358
|
+
if (reason === 'no-output' && !retriedNoOutput) {
|
|
359
|
+
const retry = await runShard(paths, shard, depth, `${branch}-retry`, true);
|
|
360
|
+
debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
|
|
361
|
+
return retry;
|
|
362
|
+
}
|
|
357
363
|
if (reason) {
|
|
358
364
|
// 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
|
|
359
365
|
// 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { join, isAbsolute } from 'node:path';
|
|
5
5
|
import { debug } from '../../src/debug/log.js';
|
|
6
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
|
|
7
7
|
|
|
8
8
|
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
|
|
9
|
-
|
|
9
|
+
/** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
|
|
10
|
+
export interface FeedSource { checkout: string; commit: string; cleanup: () => void }
|
|
11
|
+
interface OpsUpgradeDeps { exists?: (path: string) => boolean; installSource?: () => string | null; feedSource?: (version: string) => FeedSource }
|
|
12
|
+
|
|
13
|
+
/** Detached worktree at refs/tags/v<version> with the PWA built — the feed ships what was released (10-04 0.2.11:
|
|
14
|
+
* packing process.cwd() shipped main HEAD ee689ad2 as «0.2.11» to node-b·cloud-vm instead of the cut 31129c91). */
|
|
15
|
+
export function tagFeedSource(run: CommandRunner, repo: string = process.cwd()): (version: string) => FeedSource {
|
|
16
|
+
return (version) => {
|
|
17
|
+
const tag = run('git', ['rev-parse', '--verify', `refs/tags/v${version}^{commit}`], repo);
|
|
18
|
+
const commit = tag.stdout.trim();
|
|
19
|
+
if (tag.status !== 0 || !/^[0-9a-f]{40,64}$/.test(commit)) throw new Error(`태그 없음: v${version}`);
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), 'ops-feed-'));
|
|
21
|
+
const tree = join(dir, 'tree');
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
run('git', ['worktree', 'remove', '--force', tree], repo);
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
const added = run('git', ['worktree', 'add', '--detach', tree, commit], repo);
|
|
28
|
+
if (added.status !== 0) throw new Error(`태그 체크아웃 실패: ${reason(added)}`);
|
|
29
|
+
symlinkSync(join(repo, 'node_modules'), join(tree, 'node_modules'));
|
|
30
|
+
if (existsSync(join(repo, 'apps/pwa/node_modules'))) symlinkSync(join(repo, 'apps/pwa/node_modules'), join(tree, 'apps/pwa/node_modules'));
|
|
31
|
+
// The feed carries the PWA build (apps/pwa/out is ignored, so a fresh tag checkout has none until built).
|
|
32
|
+
const built = run('bun', ['bin/elanous.mjs', 'nexus', 'build'], tree, 900_000);
|
|
33
|
+
if (built.status !== 0) throw new Error(`태그 PWA 빌드 실패: ${reason(built)}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
cleanup();
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return { checkout: tree, commit, cleanup };
|
|
39
|
+
};
|
|
40
|
+
}
|
|
10
41
|
|
|
11
42
|
function localInstallSource(): string | null {
|
|
12
43
|
try {
|
|
@@ -54,11 +85,21 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
|
|
|
54
85
|
: (deps.exists ?? existsSync)(defaultFeed) ? defaultFeed : undefined;
|
|
55
86
|
let feedError = '';
|
|
56
87
|
if (feedPath !== undefined) {
|
|
88
|
+
let source: FeedSource | undefined;
|
|
57
89
|
try {
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
90
|
+
source = (deps.feedSource ?? tagFeedSource(run))(version);
|
|
91
|
+
const released = context.outputs['version-release']?.commit;
|
|
92
|
+
if (typeof released === 'string' && released && released !== source.commit) {
|
|
93
|
+
feedError = `내부 피드 발행 실패: 태그 v${version}(${source.commit.slice(0, 12)})가 컷(${released.slice(0, 12)})과 다르다`;
|
|
94
|
+
} else {
|
|
95
|
+
const feed = run('bun', ['scripts/publish-internal-dist.ts', '--checkout', source.checkout, '--out', feedPath]);
|
|
96
|
+
const data = lastResult(feed);
|
|
97
|
+
if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
|
|
98
|
+
else if (data.commit !== source.commit) feedError = `내부 피드 발행 실패: 묶은 커밋 ${String(data.commit).slice(0, 12)} ≠ 태그 ${source.commit.slice(0, 12)}`;
|
|
99
|
+
debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, packed: data?.commit, pwaBuild: data?.pwaBuild });
|
|
100
|
+
}
|
|
61
101
|
} catch (error) { feedError = `내부 피드 발행 실패: ${firstLine(String(error))}`; }
|
|
102
|
+
finally { source?.cleanup(); }
|
|
62
103
|
}
|
|
63
104
|
const results: HostResult[] = [];
|
|
64
105
|
for (const host of hosts as string[]) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { WARNING_ONLY } from './auto-approve-node.js';
|
|
5
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
|
|
6
7
|
|
|
7
8
|
export function publicNotes(markdown: string, pages: { pages: Array<{ id: string; slug?: string; source?: string }> }): string {
|
|
@@ -40,7 +41,9 @@ export function runPublish(run: CommandRunner = runCommand) {
|
|
|
40
41
|
const auto = context.outputs['auto-approve'];
|
|
41
42
|
if (context.outputs['approve-publish']?.outcome !== 'approved' &&
|
|
42
43
|
!(auto?.outcome === 'ok' && auto.decidedBy === 'release-loop metrics'
|
|
43
|
-
|
|
44
|
+
// The 8 blocking metrics; warning-only rows (tui-regress · mac-smoke) are shown but never counted (V1g · MAC1).
|
|
45
|
+
&& Array.isArray(auto.metrics)
|
|
46
|
+
&& auto.metrics.filter((metric: unknown) => !(metric && typeof metric === 'object' && 'name' in metric && WARNING_ONLY.has(String(metric.name)))).length === 8
|
|
44
47
|
&& auto.metrics.every((metric: unknown) => metric && typeof metric === 'object' && 'verdict' in metric && metric.verdict === 'pass'))) {
|
|
45
48
|
return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: neither auto-approve nor approve-publish approved' };
|
|
46
49
|
}
|
|
@@ -7,7 +7,7 @@ import { emitNodeResult } from './node-verdict.js';
|
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
8
|
|
|
9
9
|
type Kind = 'release' | 'dev-bump';
|
|
10
|
-
type Output = { outcome: 'ok' | 'error'; kind: Kind | null; version: string | null; commit: string | null; pr: number | null; worktree?: string; error?: string };
|
|
10
|
+
type Output = { outcome: 'ok' | 'error'; kind: Kind | null; version: string | null; commit: string | null; pr: number | null; files?: string[]; worktree?: string; error?: string };
|
|
11
11
|
|
|
12
12
|
export function nextDevVersion(version: string): string {
|
|
13
13
|
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
@@ -120,12 +120,22 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
|
|
|
120
120
|
const old = `"version": "${current}"`;
|
|
121
121
|
if (!text.includes(old)) throw new Error(`package.json version field not found: ${current}`);
|
|
122
122
|
writeFileSync(path, text.replace(old, `"version": "${target}"`));
|
|
123
|
+
const files = ['package.json'];
|
|
123
124
|
const serverPath = join(worktree, 'server.json');
|
|
124
125
|
if (existsSync(serverPath)) {
|
|
125
126
|
const server = JSON.parse(readFileSync(serverPath, 'utf8')) as { version: string; packages: Array<{ version: string }> };
|
|
126
127
|
server.version = target;
|
|
127
128
|
server.packages[0]!.version = target;
|
|
128
129
|
writeFileSync(serverPath, `${JSON.stringify(server, null, 2)}\n`);
|
|
130
|
+
files.push('server.json');
|
|
131
|
+
}
|
|
132
|
+
const agentPath = join(worktree, 'integrations', 'acp-registry', 'elanous', 'agent.json');
|
|
133
|
+
if (existsSync(agentPath)) {
|
|
134
|
+
const agent = JSON.parse(readFileSync(agentPath, 'utf8')) as { version: string; distribution: { npx: { package: string } } };
|
|
135
|
+
agent.version = target;
|
|
136
|
+
agent.distribution.npx.package = `elanous@${target}`;
|
|
137
|
+
writeFileSync(agentPath, `${JSON.stringify(agent, null, 2)}\n`);
|
|
138
|
+
files.push('agent.json');
|
|
129
139
|
}
|
|
130
140
|
const lockPath = join(worktree, 'bun.lock');
|
|
131
141
|
const lock = readFileSync(lockPath, 'utf8');
|
|
@@ -185,7 +195,7 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
|
|
|
185
195
|
fetchMain(repo);
|
|
186
196
|
const commit = git(repo, 'rev-parse', 'origin/main');
|
|
187
197
|
if (versionAt(repo) !== target || commit === before) throw new Error(`origin/main did not advance to ${target} after merge`);
|
|
188
|
-
const output: Output = { outcome: 'ok', kind, version: target, commit, pr: pr ? Number(pr[1]) : null };
|
|
198
|
+
const output: Output = { outcome: 'ok', kind, version: target, commit, pr: pr ? Number(pr[1]) : null, files };
|
|
189
199
|
if (kind === 'release') debug.log('release-loop.cut', 'chosen', { version: releaseVersion, source: 'main', commit });
|
|
190
200
|
git(repo, 'worktree', 'remove', '--force', worktree);
|
|
191
201
|
rmSync(temp, { recursive: true, force: true });
|
|
@@ -240,7 +250,7 @@ function main(args: string[] = process.argv.slice(2), repo = process.cwd()): Out
|
|
|
240
250
|
|
|
241
251
|
if (import.meta.main) {
|
|
242
252
|
const output = main();
|
|
243
|
-
debug.log(`release-loop.version-${output.kind ?? 'unknown'}`, 'result', { version: output.version, outcome: output.outcome });
|
|
253
|
+
debug.log(`release-loop.version-${output.kind ?? 'unknown'}`, 'result', { version: output.version, outcome: output.outcome, files: output.files ?? [] });
|
|
244
254
|
emitNodeResult({ ...output, verdict: output.outcome === 'ok' ? 'pass' : 'fail', summary: output.outcome === 'ok' ? `${output.kind} ${output.version} · ${output.commit}` : `${output.kind ?? 'version'}: ${output.error ?? 'failed'}` });
|
|
245
255
|
if (output.outcome === 'error') process.exitCode = 1;
|
|
246
256
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { TelegramClient, Api } from 'telegram';
|
|
17
17
|
import { StringSession } from 'telegram/sessions';
|
|
18
18
|
import { createInterface } from 'node:readline/promises';
|
|
19
|
+
import { parseInjectArgs } from './lib/telegram-inject-args.js';
|
|
19
20
|
|
|
20
21
|
const args = process.argv.slice(2);
|
|
21
22
|
const has = (f: string): boolean => args.includes(f);
|
|
@@ -201,15 +202,22 @@ if (has('--buttons') || has('--click') || has('--react')) {
|
|
|
201
202
|
|
|
202
203
|
// 무인 주입: SESSION STRING 으로 로그인 → 봇에게 메시지.
|
|
203
204
|
const sessionStr = process.env.TELEGRAM_USER_SESSION ?? val('--session');
|
|
204
|
-
const to = val('--to') ?? process.env.TELEGRAM_TEST_BOT ?? configBot;
|
|
205
|
-
const text = val('--text');
|
|
206
205
|
if (!sessionStr) { console.error('❌ TELEGRAM_USER_SESSION 필요 — 먼저 `--login` 으로 발급.'); process.exit(2); }
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
let inject: ReturnType<typeof parseInjectArgs>;
|
|
207
|
+
try {
|
|
208
|
+
inject = parseInjectArgs(args, { TELEGRAM_TEST_BOT: process.env.TELEGRAM_TEST_BOT ?? configBot });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error((error as Error).message);
|
|
211
|
+
process.exit(2);
|
|
212
|
+
}
|
|
213
|
+
const { to, text, photos, caption } = inject;
|
|
209
214
|
const client = new TelegramClient(new StringSession(sessionStr), apiId, apiHash, { connectionRetries: 5 });
|
|
210
215
|
await client.connect();
|
|
211
|
-
const msg =
|
|
212
|
-
|
|
216
|
+
const msg = photos.length > 0
|
|
217
|
+
? await client.sendFile(to, { file: photos.length === 1 ? photos[0]! : photos, caption })
|
|
218
|
+
: await client.sendMessage(to, { message: text! });
|
|
219
|
+
const sent = Array.isArray(msg) ? msg[0] : msg;
|
|
220
|
+
console.log(`✅ 주입 완료 → ${to} (msgId=${(sent as { id?: number } | undefined)?.id ?? '?'}). 이 계정의 텔레그램 앱에도 대화가 보입니다.`);
|
|
213
221
|
console.log(' → 데몬 처리 후 watchdog 검증: log/watchdog-stall.log (stall 없으면 A subprocess 격리 정상).');
|
|
214
222
|
await client.disconnect();
|
|
215
223
|
process.exit(0);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { release } from 'node:os';
|
|
5
|
+
|
|
6
|
+
export const FILE_TIMEOUT_MS = 300_000;
|
|
7
|
+
export type Execution = { rc: number | null; stdout: string; stderr: string; secs: number; timedOut: boolean };
|
|
8
|
+
export type Executor = (file: string, timeoutMs: number) => Promise<Execution>;
|
|
9
|
+
export type Measurement = {
|
|
10
|
+
file: string; rc: number | null; pass: number | null; fail: number | null; skip: number | null;
|
|
11
|
+
secs: number; platform: string; release: string; verdict: 'pass' | 'fail' | 'skip' | '못 잼';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const TIMEOUT_EXIT_GRACE_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
// The wrapper handles SIGTERM by stopping its detached test group. Do not
|
|
17
|
+
// start another file until the wrapper closes, even after SIGKILL.
|
|
18
|
+
export function execute(
|
|
19
|
+
file: string,
|
|
20
|
+
timeoutMs: number,
|
|
21
|
+
deps: {
|
|
22
|
+
spawnChild?: typeof spawn;
|
|
23
|
+
signalGroup?: (pid: number, signal: NodeJS.Signals) => void;
|
|
24
|
+
graceMs?: number;
|
|
25
|
+
} = {},
|
|
26
|
+
): Promise<Execution> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const started = performance.now();
|
|
29
|
+
const child = (deps.spawnChild ?? spawn)('bun', ['run', 'test:deterministic', file], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
let timedOut = false;
|
|
33
|
+
let closed = false;
|
|
34
|
+
let escalation: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let terminationCheck: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
const seconds = () => Math.round((performance.now() - started) / 1000 * 100) / 100;
|
|
37
|
+
const signal = (name: NodeJS.Signals) => {
|
|
38
|
+
if (child.pid === undefined) return;
|
|
39
|
+
try { (deps.signalGroup ?? ((pid, sig) => process.kill(-pid, sig)))(child.pid, name); }
|
|
40
|
+
catch (groupError) {
|
|
41
|
+
try { child.kill(name); }
|
|
42
|
+
catch (directError) { stderr += `\n${name} failed: ${String(groupError)}; ${String(directError)}`; }
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// Bun's summary is at the end; bounded capture avoids an unbounded memory cost.
|
|
46
|
+
const collect = (previous: string, chunk: Buffer) => (previous + chunk.toString('utf8')).slice(-256_000);
|
|
47
|
+
child.stdout.on('data', (chunk: Buffer) => { stdout = collect(stdout, chunk); });
|
|
48
|
+
child.stderr.on('data', (chunk: Buffer) => { stderr = collect(stderr, chunk); });
|
|
49
|
+
const graceMs = deps.graceMs ?? TIMEOUT_EXIT_GRACE_MS;
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
timedOut = true;
|
|
52
|
+
signal('SIGTERM');
|
|
53
|
+
escalation = setTimeout(() => {
|
|
54
|
+
if (closed) return;
|
|
55
|
+
signal('SIGKILL');
|
|
56
|
+
terminationCheck = setTimeout(() => {
|
|
57
|
+
if (!closed) reject(new Error(`Cannot confirm termination of timed-out test: ${file}; stopping measurements`));
|
|
58
|
+
}, graceMs);
|
|
59
|
+
}, graceMs);
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
child.on('error', (error) => { stderr += `\n${error.message}`; });
|
|
62
|
+
child.on('close', (code) => {
|
|
63
|
+
closed = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
if (escalation) clearTimeout(escalation);
|
|
66
|
+
if (terminationCheck) clearTimeout(terminationCheck);
|
|
67
|
+
resolve({ rc: timedOut ? null : code, stdout, stderr, secs: seconds(), timedOut });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function measureList(files: readonly string[], runner: Executor = execute): Promise<Measurement[]> {
|
|
73
|
+
const measurements: Measurement[] = [];
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const result = await runner(file, FILE_TIMEOUT_MS);
|
|
76
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
77
|
+
const count = (kind: 'pass' | 'fail' | 'skip'): number | null => {
|
|
78
|
+
const matches = [...output.matchAll(new RegExp(`^\\s*(\\d+) ${kind}\\b`, 'gm'))];
|
|
79
|
+
return matches.length ? Number(matches.at(-1)![1]) : null;
|
|
80
|
+
};
|
|
81
|
+
const pass = result.timedOut ? null : count('pass');
|
|
82
|
+
const fail = result.timedOut ? null : count('fail');
|
|
83
|
+
const skip = result.timedOut ? null : count('skip');
|
|
84
|
+
const measured = pass !== null || fail !== null || skip !== null;
|
|
85
|
+
const verdict = result.timedOut || !measured || result.rc === null ? '못 잼'
|
|
86
|
+
: fail !== null && fail > 0 ? 'fail'
|
|
87
|
+
: result.rc !== 0 ? '못 잼'
|
|
88
|
+
: skip !== null && skip > 0 ? 'skip'
|
|
89
|
+
: pass !== null && pass > 0 ? 'pass' : '못 잼';
|
|
90
|
+
measurements.push({ file, rc: result.rc, pass, fail, skip, secs: result.secs, platform: process.platform, release: release(), verdict });
|
|
91
|
+
}
|
|
92
|
+
return measurements;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (import.meta.main) {
|
|
96
|
+
const args = process.argv.slice(2);
|
|
97
|
+
if (args.length < 1 || args.length > 2 || (args.length === 2 && args[1] !== '--json')) {
|
|
98
|
+
console.error('usage: bun scripts/test-diet/remeasure-list.ts <목록 파일> [--json]');
|
|
99
|
+
process.exitCode = 2;
|
|
100
|
+
} else {
|
|
101
|
+
try {
|
|
102
|
+
const files = readFileSync(args[0]!, 'utf8').split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
|
|
103
|
+
if (!files.length) throw new Error('empty test list');
|
|
104
|
+
const results = await measureList(files);
|
|
105
|
+
if (args[1] === '--json') console.log(JSON.stringify(results, null, 2));
|
|
106
|
+
else for (const row of results) console.log([row.file, row.rc ?? 'null', row.pass ?? '?', row.fail ?? '?', row.skip ?? '?', row.secs, row.platform, row.release, row.verdict].join('\t'));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(error);
|
|
109
|
+
process.exitCode = 2;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -5,6 +5,11 @@ import { Frames, launch, PUBLIC_LEAK_CHECK, sleep } from './lib/cdp.js';
|
|
|
5
5
|
import { failedApiResponse, judgeRun, judgeScene, worstSceneState, type SceneObservation, type SceneState } from './lib/rehearsal-verdict.js';
|
|
6
6
|
|
|
7
7
|
const TITLES = ['문서 아키텍처', '라이브 트레이스', '루프 에이전트', '그래프 편집기', '마법사 → 마켓', 'PTY 인텔리전스'];
|
|
8
|
+
export const EMPTY_STATE_PHRASES = ['지금 도는 런이 없습니다', '마법사가 아직 돌지 않았습니다', '터미널을 기다리는 중', '판단을 기다리는 중', '못 읽음'] as const;
|
|
9
|
+
|
|
10
|
+
export function sceneSnapshotScript(): string {
|
|
11
|
+
return `(()=>{const sections=[...document.querySelectorAll('[data-inside-scene]')]; const visible=sections.filter(s=>!s.hidden && s.getClientRects().length>0); const text=visible.length===1?visible[0].innerText:''; return {sectionsInDom:sections.length,visibleScene:visible.length===1?Number(visible[0].getAttribute('data-inside-scene')):null,textLength:text.length,emptyStates:${JSON.stringify(EMPTY_STATE_PHRASES)}.filter(phrase=>text.includes(phrase))}})()`;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
type Args = { url: string; out: string; secs: number; width: number; height: number; demo: boolean; tokenStdin: boolean };
|
|
10
15
|
|
|
@@ -168,23 +173,20 @@ export async function run(args: Args): Promise<number> {
|
|
|
168
173
|
const initialEvents = observer.take();
|
|
169
174
|
const scenes = [];
|
|
170
175
|
for (let scene = 1; scene <= 6; scene++) {
|
|
171
|
-
const hiddenByDemo =
|
|
172
|
-
if (hiddenByDemo) {
|
|
173
|
-
const result = judgeScene({ scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: 0, frames: 0, exceptions: [], failedRequests: [], textLength: 0, leaks: [], sectionsInDom: 0, visibleScene: null });
|
|
174
|
-
scenes.push(result);
|
|
175
|
-
console.log(`scene ${scene} ${result.verdict} 0s frames=0`);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
176
|
+
const hiddenByDemo = false;
|
|
178
177
|
if (scene !== 1) {
|
|
179
178
|
await cdp.ev<boolean>(`(()=>{const n=${scene}; const nav=document.querySelector('nav[aria-label="장면 선택"]'); const button=[...(nav?.querySelectorAll('button')??[])].find(b=>b.textContent?.trim().startsWith(n+' ')); button?.click(); return !!button})()`);
|
|
180
179
|
await sleep(300);
|
|
181
180
|
}
|
|
182
181
|
const frames = new Frames(join(args.out, `scene-${scene}`));
|
|
183
182
|
const states: SceneState[] = [];
|
|
183
|
+
const emptyStates = new Set<string>();
|
|
184
184
|
const sceneRect = await cdp.ev<FrameRect | null>(`(()=>{const s=document.querySelector('[data-inside-scene="${scene}"]'); if (!s || s.hidden) return null; const r=s.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height}})()`);
|
|
185
185
|
let blankFrames = 0;
|
|
186
186
|
const inspect = async () => {
|
|
187
|
-
|
|
187
|
+
const { emptyStates: observedEmpty, ...state } = await cdp.ev<SceneState & { emptyStates: string[] }>(sceneSnapshotScript());
|
|
188
|
+
states.push(state);
|
|
189
|
+
for (const phrase of observedEmpty) emptyStates.add(phrase);
|
|
188
190
|
};
|
|
189
191
|
const until = Date.now() + args.secs * 1000;
|
|
190
192
|
await inspect();
|
|
@@ -203,7 +205,7 @@ export async function run(args: Args): Promise<number> {
|
|
|
203
205
|
failedRequests: [...initialEvents.failedRequests, ...observed.failedRequests],
|
|
204
206
|
} : observed;
|
|
205
207
|
frames.toMp4(join(args.out, `scene-${scene}.mp4`), args.width, args.height);
|
|
206
|
-
const obs: SceneObservation = { scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: args.secs, frames: frames.list.length, ...events, ...dom, leaks: bad, blankFrames };
|
|
208
|
+
const obs: SceneObservation = { scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: args.secs, frames: frames.list.length, ...events, ...dom, leaks: bad, blankFrames, emptyStates: [...emptyStates] };
|
|
207
209
|
const result = judgeScene(obs);
|
|
208
210
|
scenes.push(result);
|
|
209
211
|
console.log(`scene ${scene} ${result.verdict} ${args.secs}s frames=${result.frames}`);
|
|
@@ -222,13 +224,15 @@ export async function run(args: Args): Promise<number> {
|
|
|
222
224
|
'|---|---|---:|---:|---|',
|
|
223
225
|
...scenes.map((s) => `| ${s.scene} ${s.title} | ${s.verdict} | ${s.secs} | ${s.frames} | ${s.reasons.join('; ') || '없음'} |`),
|
|
224
226
|
'',
|
|
225
|
-
`ok ${result.ok} · broken ${result.broken} · 미검증 ${result.unverified}`,
|
|
227
|
+
`ok ${result.ok} · broken ${result.broken} · 미검증 ${result.unverified} · 실데이터 없음 ${result.noData}`,
|
|
228
|
+
'',
|
|
229
|
+
'종료 코드: ok=0 · broken=1 · unverified=2 · no-data=3',
|
|
226
230
|
'',
|
|
227
231
|
].join('\n'));
|
|
228
232
|
for (const line of result.banner) console.log(`⚠️ ${line}`);
|
|
229
|
-
console.log(`rehearsal ${result.verdict} ok=${result.ok} broken=${result.broken} unverified=${result.unverified}`);
|
|
230
|
-
// 0 =
|
|
231
|
-
return result.verdict === 'broken' ? 1 : result.verdict === 'unverified' ? 2 : 0;
|
|
233
|
+
console.log(`rehearsal ${result.verdict} ok=${result.ok} broken=${result.broken} unverified=${result.unverified} no-data=${result.noData}`);
|
|
234
|
+
// 0 = ok · 1 = broken · 2 = unverified · 3 = no-data.
|
|
235
|
+
return result.verdict === 'broken' ? 1 : result.verdict === 'unverified' ? 2 : result.verdict === 'no-data' ? 3 : 0;
|
|
232
236
|
} finally { observer?.close(); cdp.close(); }
|
|
233
237
|
}
|
|
234
238
|
|