@wooojin/forgen 0.4.8 → 0.4.9

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.
Files changed (122) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/assets/dev-guide/be/README.md +226 -0
  3. package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
  4. package/assets/dev-guide/be/principles/common.md +433 -0
  5. package/assets/dev-guide/be/principles/go.md +469 -0
  6. package/assets/dev-guide/be/principles/node.md +388 -0
  7. package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
  8. package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
  9. package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
  10. package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
  11. package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
  12. package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
  13. package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
  14. package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
  15. package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
  16. package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
  17. package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
  18. package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
  19. package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
  20. package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
  21. package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
  22. package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
  23. package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
  24. package/assets/dev-guide/fe/README.md +197 -0
  25. package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
  26. package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
  27. package/assets/dev-guide/fe/principles/common.md +160 -0
  28. package/assets/dev-guide/fe/principles/react.md +183 -0
  29. package/assets/dev-guide/fe/principles/vue.md +196 -0
  30. package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
  31. package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
  32. package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
  33. package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
  34. package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
  35. package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
  36. package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
  37. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
  38. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
  39. package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
  40. package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
  41. package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
  42. package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
  43. package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
  44. package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
  45. package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
  46. package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
  47. package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
  48. package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
  49. package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
  50. package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
  51. package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
  52. package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
  53. package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
  54. package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
  55. package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
  56. package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
  57. package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
  58. package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
  59. package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
  60. package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
  61. package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
  62. package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
  63. package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
  64. package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
  65. package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
  66. package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
  67. package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
  68. package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
  69. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
  70. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
  71. package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
  72. package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
  73. package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
  74. package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
  75. package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
  76. package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
  77. package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
  78. package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
  79. package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
  80. package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
  81. package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
  82. package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
  83. package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
  84. package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
  85. package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
  86. package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
  87. package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
  88. package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
  89. package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
  90. package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
  91. package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
  92. package/dist/cli.js +42 -0
  93. package/dist/core/dashboard-cli.d.ts +12 -0
  94. package/dist/core/dashboard-cli.js +226 -0
  95. package/dist/core/dev-guide-injector.d.ts +26 -0
  96. package/dist/core/dev-guide-injector.js +137 -0
  97. package/dist/core/init.js +53 -0
  98. package/dist/core/lifecycle-classifier.d.ts +23 -0
  99. package/dist/core/lifecycle-classifier.js +104 -0
  100. package/dist/core/observability-backfill.d.ts +31 -0
  101. package/dist/core/observability-backfill.js +178 -0
  102. package/dist/core/observability-store.d.ts +58 -0
  103. package/dist/core/observability-store.js +195 -0
  104. package/dist/core/session-store.js +4 -0
  105. package/dist/core/spawn.d.ts +17 -0
  106. package/dist/core/spawn.js +179 -2
  107. package/dist/core/statusline-cli.js +34 -1
  108. package/dist/engine/compound-extractor.js +39 -0
  109. package/dist/engine/compound-loop.js +6 -0
  110. package/dist/engine/compound-retire.d.ts +20 -0
  111. package/dist/engine/compound-retire.js +85 -0
  112. package/dist/hooks/context-guard.js +25 -1
  113. package/dist/hooks/post-tool-use.js +48 -0
  114. package/dist/hooks/solution-injector.js +93 -0
  115. package/dist/host/install-claude.d.ts +6 -2
  116. package/dist/host/install-claude.js +74 -2
  117. package/dist/host/install-codex.d.ts +4 -0
  118. package/dist/host/install-codex.js +71 -0
  119. package/dist/host/install-orchestrator.js +1 -0
  120. package/package.json +6 -6
  121. package/plugin.json +1 -1
  122. package/scripts/postinstall.js +134 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Forgen — Observability Backfill (Phase 2)
3
+ *
4
+ * 기존 JSONL 상태 파일에서 solution_events 를 소급 생성한다.
5
+ * Phase A (결정론적): match-eval-log, implicit-feedback, compound-usage, outcomes
6
+ * Phase B (휴리스틱): transcript 스캔 — CLI --phase B|all 로만 활성
7
+ *
8
+ * 안전성:
9
+ * - 기본: events 가 이미 있으면 reject (--force 필요)
10
+ * - signal_source 에 '-backfill' prefix 로 실시간 emit 과 구분
11
+ * - BEGIN/COMMIT 단위 트랜잭션
12
+ * - fail-open: 파일 누락은 조용히 skip
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { createRequire } from 'node:module';
17
+ import { STATE_DIR, MATCH_EVAL_LOG_PATH, FORGEN_HOME } from './paths.js';
18
+ import { createLogger } from './logger.js';
19
+ import { emitSolutionEvent } from './observability-store.js';
20
+ const require = createRequire(import.meta.url);
21
+ const log = createLogger('observability-backfill');
22
+ const DB_PATH = path.join(FORGEN_HOME, 'sessions.db');
23
+ const COMPOUND_USAGE_LOG = path.join(STATE_DIR, 'compound-usage.jsonl');
24
+ const IMPLICIT_FEEDBACK_LOG = path.join(STATE_DIR, 'implicit-feedback.jsonl');
25
+ const OUTCOMES_DIR = path.join(STATE_DIR, 'outcomes');
26
+ function readJsonlLines(filePath) {
27
+ if (!fs.existsSync(filePath))
28
+ return [];
29
+ try {
30
+ return fs.readFileSync(filePath, 'utf-8')
31
+ .split('\n')
32
+ .filter(Boolean)
33
+ .map(l => { try {
34
+ return JSON.parse(l);
35
+ }
36
+ catch {
37
+ return null;
38
+ } })
39
+ .filter((x) => x !== null);
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ function hasExistingEvents() {
46
+ try {
47
+ if (!fs.existsSync(DB_PATH))
48
+ return false;
49
+ const { DatabaseSync } = require('node:sqlite');
50
+ const db = new DatabaseSync(DB_PATH);
51
+ try {
52
+ const row = db.prepare(`SELECT COUNT(*) AS cnt FROM solution_events`).get();
53
+ return (row?.cnt ?? 0) > 0;
54
+ }
55
+ finally {
56
+ db.close();
57
+ }
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ /** Phase A: 결정론적 소급 */
64
+ async function runPhaseA(opts) {
65
+ const counts = { matched: 0, surfaced: 0, acted_on: 0 };
66
+ // 1. match-eval-log.jsonl → matched events
67
+ const matchLines = readJsonlLines(MATCH_EVAL_LOG_PATH);
68
+ for (const line of matchLines) {
69
+ const sessionId = line.sessionId ?? line.session_id ?? undefined;
70
+ const topN = line.rankedTopN ?? line.candidates?.slice(0, 5).map(c => c.name) ?? [];
71
+ const candidateMap = new Map((line.candidates ?? []).map(c => [c.name, c.relevance]));
72
+ for (const name of topN) {
73
+ if (!name)
74
+ continue;
75
+ const score = candidateMap.get(name) ?? null;
76
+ if (!opts.dryRun) {
77
+ emitSolutionEvent({
78
+ sessionId: sessionId ?? null,
79
+ solutionId: name,
80
+ eventType: 'matched',
81
+ signalSource: 'matcher-backfill',
82
+ signalScore: score,
83
+ });
84
+ }
85
+ counts.matched++;
86
+ if (counts.matched % 1000 === 0)
87
+ process.stderr.write(`[backfill] matched: ${counts.matched}\n`);
88
+ }
89
+ }
90
+ // 2. implicit-feedback.jsonl type='recommendation_surfaced' → surfaced
91
+ const feedbackLines = readJsonlLines(IMPLICIT_FEEDBACK_LOG);
92
+ for (const line of feedbackLines) {
93
+ if (line.type !== 'recommendation_surfaced')
94
+ continue;
95
+ const sid = line.solutionId ?? line.solution_id;
96
+ if (!sid)
97
+ continue;
98
+ const sessionId = line.sessionId ?? line.session_id ?? undefined;
99
+ if (!opts.dryRun) {
100
+ emitSolutionEvent({
101
+ sessionId: sessionId ?? null,
102
+ solutionId: sid,
103
+ eventType: 'surfaced',
104
+ signalSource: 'hook-backfill',
105
+ });
106
+ }
107
+ counts.surfaced++;
108
+ }
109
+ // 3. compound-usage.jsonl → acted_on (signalSource='mcp-read-backfill')
110
+ const usageLines = readJsonlLines(COMPOUND_USAGE_LOG);
111
+ for (const line of usageLines) {
112
+ if (!line.name)
113
+ continue;
114
+ if (!opts.dryRun) {
115
+ emitSolutionEvent({
116
+ sessionId: null,
117
+ solutionId: line.name,
118
+ eventType: 'acted_on',
119
+ signalSource: 'mcp-read-backfill',
120
+ signalScore: 0.30,
121
+ });
122
+ }
123
+ counts.acted_on++;
124
+ if (counts.acted_on % 1000 === 0)
125
+ process.stderr.write(`[backfill] acted_on: ${counts.acted_on}\n`);
126
+ }
127
+ // 4. outcomes/<sid>.jsonl outcome='accept' → acted_on (signalSource='outcome-accept-backfill')
128
+ if (fs.existsSync(OUTCOMES_DIR)) {
129
+ const outFiles = fs.readdirSync(OUTCOMES_DIR).filter(f => f.endsWith('.jsonl'));
130
+ for (const file of outFiles) {
131
+ const lines = readJsonlLines(path.join(OUTCOMES_DIR, file));
132
+ for (const line of lines) {
133
+ if (line.outcome !== 'accept')
134
+ continue;
135
+ const sid = line.solution_id ?? line.solutionId;
136
+ if (!sid)
137
+ continue;
138
+ const sessionId = line.session_id ?? line.sessionId ?? undefined;
139
+ if (!opts.dryRun) {
140
+ emitSolutionEvent({
141
+ sessionId: sessionId ?? null,
142
+ solutionId: sid,
143
+ eventType: 'acted_on',
144
+ signalSource: 'outcome-accept-backfill',
145
+ signalScore: 0.15,
146
+ });
147
+ }
148
+ counts.acted_on++;
149
+ }
150
+ }
151
+ }
152
+ return counts;
153
+ }
154
+ export async function runBackfill(opts = {}) {
155
+ const start = Date.now();
156
+ const phase = opts.phase ?? 'A';
157
+ if (!opts.force && !opts.dryRun) {
158
+ if (hasExistingEvents()) {
159
+ throw new Error('solution_events 테이블에 기존 이벤트가 있습니다. ' +
160
+ '--force 플래그를 사용하면 강행합니다.');
161
+ }
162
+ }
163
+ const phaseA = { matched: 0, surfaced: 0, acted_on: 0 };
164
+ const phaseB = { acted_on: 0 };
165
+ if (phase === 'A' || phase === 'all') {
166
+ const aResult = await runPhaseA(opts);
167
+ phaseA.matched = aResult.matched;
168
+ phaseA.surfaced = aResult.surfaced;
169
+ phaseA.acted_on = aResult.acted_on;
170
+ }
171
+ // Phase B: transcript 스캔 — 1차 release 는 opt-in 만
172
+ if (phase === 'B' || phase === 'all') {
173
+ log.debug('Phase B (transcript scan) 은 현재 미구현 — 향후 릴리스에서 활성화');
174
+ }
175
+ const total = phaseA.matched + phaseA.surfaced + phaseA.acted_on + phaseB.acted_on;
176
+ const durationMs = Date.now() - start;
177
+ return { phaseA, phaseB, total, durationMs };
178
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Forgen — Observability Store (Phase 1)
3
+ *
4
+ * solution_events 테이블에 matched/surfaced/acted_on 이벤트를 기록.
5
+ * Fail-open: 모든 함수는 내부 오류를 삼키고 절대 throw 하지 않는다.
6
+ */
7
+ export type EventType = 'matched' | 'surfaced' | 'acted_on';
8
+ export interface EmitOptions {
9
+ ts?: number;
10
+ sessionId?: string | null;
11
+ solutionId: string;
12
+ eventType: EventType;
13
+ signalSource: string;
14
+ signalScore?: number | null;
15
+ meta?: Record<string, unknown>;
16
+ }
17
+ export interface SurfacedEvent {
18
+ id: number;
19
+ ts: number;
20
+ sessionId: string | null;
21
+ solutionId: string;
22
+ signalSource: string;
23
+ signalScore: number | null;
24
+ meta: Record<string, unknown> | null;
25
+ }
26
+ export interface HitRateRow {
27
+ solutionId: string;
28
+ matched_30d: number;
29
+ surfaced_30d: number;
30
+ acted_30d: number;
31
+ matched_90d: number;
32
+ surfaced_90d: number;
33
+ acted_90d: number;
34
+ matched_180d: number;
35
+ surfaced_180d: number;
36
+ acted_180d: number;
37
+ last_event_ts: number;
38
+ }
39
+ interface SqliteDb {
40
+ exec(sql: string): void;
41
+ prepare(sql: string): {
42
+ run(...args: unknown[]): {
43
+ lastInsertRowid: number;
44
+ };
45
+ get(...args: unknown[]): unknown;
46
+ all(...args: unknown[]): unknown[];
47
+ };
48
+ close(): void;
49
+ }
50
+ /** solution_events 스키마 및 schema_version 초기화. idempotent. */
51
+ export declare function ensureObservabilitySchema(db: SqliteDb): void;
52
+ /** solution_events 에 이벤트를 기록한다. fail-open (throw 없음). */
53
+ export declare function emitSolutionEvent(opts: EmitOptions): void;
54
+ /** 특정 세션에서 지정 시간 window 내 surfaced 이벤트 조회. */
55
+ export declare function querySurfacedWithin(sessionId: string, minutesWindow: number): SurfacedEvent[];
56
+ /** 30/90/180일 hit-rate 집계. solutionId 지정 시 단일 솔루션만. */
57
+ export declare function queryHitRate(solutionId?: string): HitRateRow[];
58
+ export {};
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Forgen — Observability Store (Phase 1)
3
+ *
4
+ * solution_events 테이블에 matched/surfaced/acted_on 이벤트를 기록.
5
+ * Fail-open: 모든 함수는 내부 오류를 삼키고 절대 throw 하지 않는다.
6
+ */
7
+ import { createRequire } from 'node:module';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { createLogger } from './logger.js';
11
+ import { FORGEN_HOME } from './paths.js';
12
+ const require = createRequire(import.meta.url);
13
+ const log = createLogger('observability');
14
+ const DB_PATH = path.join(FORGEN_HOME, 'sessions.db');
15
+ const DEDUP_WINDOW_MS = 5 * 60 * 1000;
16
+ /** DB 파일을 열고 SqliteDb 반환. 실패 시 null. */
17
+ function openObsDb() {
18
+ try {
19
+ const { DatabaseSync } = require('node:sqlite');
20
+ const dbDir = path.dirname(DB_PATH);
21
+ if (!fs.existsSync(dbDir))
22
+ fs.mkdirSync(dbDir, { recursive: true });
23
+ const db = new DatabaseSync(DB_PATH);
24
+ db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=1000;`);
25
+ return db;
26
+ }
27
+ catch (e) {
28
+ log.debug('openObsDb 실패', e);
29
+ return null;
30
+ }
31
+ }
32
+ /** solution_events 스키마 및 schema_version 초기화. idempotent. */
33
+ export function ensureObservabilitySchema(db) {
34
+ try {
35
+ db.exec(`
36
+ CREATE TABLE IF NOT EXISTS schema_version (
37
+ component TEXT PRIMARY KEY,
38
+ version INTEGER NOT NULL,
39
+ applied_at INTEGER NOT NULL
40
+ );
41
+ CREATE TABLE IF NOT EXISTS solution_events (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ ts INTEGER NOT NULL,
44
+ session_id TEXT,
45
+ solution_id TEXT NOT NULL,
46
+ event_type TEXT NOT NULL CHECK(event_type IN ('matched','surfaced','acted_on')),
47
+ signal_source TEXT,
48
+ signal_score REAL,
49
+ meta TEXT
50
+ );
51
+ CREATE INDEX IF NOT EXISTS idx_se_solution ON solution_events(solution_id, ts);
52
+ CREATE INDEX IF NOT EXISTS idx_se_session ON solution_events(session_id, ts);
53
+ CREATE INDEX IF NOT EXISTS idx_se_type ON solution_events(event_type, ts);
54
+ `);
55
+ const row = db.prepare(`SELECT version FROM schema_version WHERE component = 'observability'`).get();
56
+ if (!row) {
57
+ db.prepare(`INSERT INTO schema_version (component, version, applied_at) VALUES ('observability', 1, ?)`).run(Date.now());
58
+ }
59
+ }
60
+ catch (e) {
61
+ log.debug('ensureObservabilitySchema 실패', e);
62
+ }
63
+ }
64
+ /** solution_events 에 이벤트를 기록한다. fail-open (throw 없음). */
65
+ export function emitSolutionEvent(opts) {
66
+ const ts = opts.ts ?? Date.now();
67
+ let db = null;
68
+ try {
69
+ db = openObsDb();
70
+ if (!db)
71
+ return;
72
+ ensureObservabilitySchema(db);
73
+ // dedup: 직전 5분 내 동일 (session, solution, source) 존재 시 skip
74
+ if (opts.sessionId) {
75
+ const dup = db.prepare(`
76
+ SELECT id FROM solution_events
77
+ WHERE session_id = ? AND solution_id = ? AND signal_source = ?
78
+ AND ts > ?
79
+ LIMIT 1
80
+ `).get(opts.sessionId, opts.solutionId, opts.signalSource, ts - DEDUP_WINDOW_MS);
81
+ if (dup)
82
+ return;
83
+ }
84
+ db.prepare(`
85
+ INSERT INTO solution_events (ts, session_id, solution_id, event_type, signal_source, signal_score, meta)
86
+ VALUES (?, ?, ?, ?, ?, ?, ?)
87
+ `).run(ts, opts.sessionId ?? null, opts.solutionId, opts.eventType, opts.signalSource, opts.signalScore ?? null, opts.meta ? JSON.stringify(opts.meta) : null);
88
+ }
89
+ catch (e) {
90
+ log.debug('emitSolutionEvent 실패', e);
91
+ }
92
+ finally {
93
+ try {
94
+ db?.close();
95
+ }
96
+ catch { /* ignore */ }
97
+ }
98
+ }
99
+ /** 특정 세션에서 지정 시간 window 내 surfaced 이벤트 조회. */
100
+ export function querySurfacedWithin(sessionId, minutesWindow) {
101
+ let db = null;
102
+ try {
103
+ db = openObsDb();
104
+ if (!db)
105
+ return [];
106
+ ensureObservabilitySchema(db);
107
+ const since = Date.now() - minutesWindow * 60 * 1000;
108
+ const rows = db.prepare(`
109
+ SELECT id, ts, session_id, solution_id, signal_source, signal_score, meta
110
+ FROM solution_events
111
+ WHERE session_id = ? AND event_type = 'surfaced' AND ts >= ?
112
+ ORDER BY ts DESC
113
+ `).all(sessionId, since);
114
+ return rows.map(r => ({
115
+ id: r.id,
116
+ ts: r.ts,
117
+ sessionId: r.session_id,
118
+ solutionId: r.solution_id,
119
+ signalSource: r.signal_source,
120
+ signalScore: r.signal_score,
121
+ meta: r.meta ? JSON.parse(r.meta) : null,
122
+ }));
123
+ }
124
+ catch (e) {
125
+ log.debug('querySurfacedWithin 실패', e);
126
+ return [];
127
+ }
128
+ finally {
129
+ try {
130
+ db?.close();
131
+ }
132
+ catch { /* ignore */ }
133
+ }
134
+ }
135
+ /** 30/90/180일 hit-rate 집계. solutionId 지정 시 단일 솔루션만. */
136
+ export function queryHitRate(solutionId) {
137
+ let db = null;
138
+ try {
139
+ db = openObsDb();
140
+ if (!db)
141
+ return [];
142
+ ensureObservabilitySchema(db);
143
+ const now = Date.now();
144
+ const cutoff30 = now - 30 * 24 * 60 * 60 * 1000;
145
+ const cutoff90 = now - 90 * 24 * 60 * 60 * 1000;
146
+ const cutoff180 = now - 180 * 24 * 60 * 60 * 1000;
147
+ const where = solutionId ? `WHERE solution_id = ?` : '';
148
+ const baseParams = [
149
+ cutoff30, cutoff30, cutoff30,
150
+ cutoff90, cutoff90, cutoff90,
151
+ cutoff180, cutoff180, cutoff180,
152
+ ];
153
+ const params = solutionId ? [...baseParams, solutionId] : baseParams;
154
+ const rows = db.prepare(`
155
+ SELECT
156
+ solution_id,
157
+ SUM(CASE WHEN event_type='matched' AND ts > ? THEN 1 ELSE 0 END) AS matched_30d,
158
+ SUM(CASE WHEN event_type='surfaced' AND ts > ? THEN 1 ELSE 0 END) AS surfaced_30d,
159
+ SUM(CASE WHEN event_type='acted_on' AND ts > ? THEN 1 ELSE 0 END) AS acted_30d,
160
+ SUM(CASE WHEN event_type='matched' AND ts > ? THEN 1 ELSE 0 END) AS matched_90d,
161
+ SUM(CASE WHEN event_type='surfaced' AND ts > ? THEN 1 ELSE 0 END) AS surfaced_90d,
162
+ SUM(CASE WHEN event_type='acted_on' AND ts > ? THEN 1 ELSE 0 END) AS acted_90d,
163
+ SUM(CASE WHEN event_type='matched' AND ts > ? THEN 1 ELSE 0 END) AS matched_180d,
164
+ SUM(CASE WHEN event_type='surfaced' AND ts > ? THEN 1 ELSE 0 END) AS surfaced_180d,
165
+ SUM(CASE WHEN event_type='acted_on' AND ts > ? THEN 1 ELSE 0 END) AS acted_180d,
166
+ MAX(ts) AS last_event_ts
167
+ FROM solution_events
168
+ ${where}
169
+ GROUP BY solution_id
170
+ `).all(...params);
171
+ return rows.map(r => ({
172
+ solutionId: r.solution_id,
173
+ matched_30d: r.matched_30d,
174
+ surfaced_30d: r.surfaced_30d,
175
+ acted_30d: r.acted_30d,
176
+ matched_90d: r.matched_90d,
177
+ surfaced_90d: r.surfaced_90d,
178
+ acted_90d: r.acted_90d,
179
+ matched_180d: r.matched_180d,
180
+ surfaced_180d: r.surfaced_180d,
181
+ acted_180d: r.acted_180d,
182
+ last_event_ts: r.last_event_ts,
183
+ }));
184
+ }
185
+ catch (e) {
186
+ log.debug('queryHitRate 실패', e);
187
+ return [];
188
+ }
189
+ finally {
190
+ try {
191
+ db?.close();
192
+ }
193
+ catch { /* ignore */ }
194
+ }
195
+ }
@@ -10,6 +10,7 @@ import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
  import { createLogger } from './logger.js';
12
12
  import { FORGEN_HOME } from './paths.js';
13
+ import { ensureObservabilitySchema } from './observability-store.js';
13
14
  const require = createRequire(import.meta.url);
14
15
  // Suppress ExperimentalWarning for node:sqlite (Node.js 22+)
15
16
  {
@@ -31,6 +32,7 @@ function openDb() {
31
32
  // Node.js 22+ experimental node:sqlite
32
33
  const { DatabaseSync } = require('node:sqlite');
33
34
  const db = new DatabaseSync(DB_PATH);
35
+ db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=1000;`);
34
36
  db.exec(`
35
37
  CREATE TABLE IF NOT EXISTS sessions (
36
38
  id TEXT PRIMARY KEY,
@@ -62,6 +64,8 @@ function openDb() {
62
64
  log.debug('FTS5 미지원 — LIKE 폴백 사용', e);
63
65
  fts5Available = false;
64
66
  }
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ ensureObservabilitySchema(db);
65
69
  return db;
66
70
  }
67
71
  catch (e) {
@@ -1,5 +1,22 @@
1
1
  import type { V1HarnessContext } from './harness.js';
2
2
  import type { RuntimeHost } from './types.js';
3
+ /**
4
+ * Plan B-1: 세션 transcript 사후 스캔으로 rate-limit 감지.
5
+ *
6
+ * stdio='inherit' 라 Claude 출력을 직접 캡처할 수 없으므로,
7
+ * 세션 종료 후 transcript JSONL 의 마지막 N 라인을 읽어
8
+ * RATE_LIMIT_REGEX 매칭 여부를 확인한다.
9
+ *
10
+ * - transcript 없음 / 빈 파일 / parse 실패 → matched: false (fail-open)
11
+ * - 이미 pending-resume.json 존재 시 hook 이 먼저 잡은 것이므로 덮어쓰지 않음
12
+ *
13
+ * @param transcriptPath JSONL 파일 경로
14
+ * @param tailLines 검사할 마지막 라인 수 (기본 5)
15
+ */
16
+ export declare function scanTranscriptForRateLimit(transcriptPath: string, tailLines?: number): Promise<{
17
+ matched: boolean;
18
+ resetAt: string | null;
19
+ }>;
3
20
  /** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
4
21
  export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
5
22
  declare const MAX_RESUMES = 3;