@gotcos/glasses-server 6.77.0 → 6.78.1

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 (34) hide show
  1. package/.env.example +40 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +12 -1
  4. package/managed-runtime-contract.json +9 -1
  5. package/package.json +10 -1
  6. package/server/index.ts +96 -10
  7. package/server/lib/agent-session-binding-registry.ts +623 -9
  8. package/server/lib/boot-resume.ts +552 -0
  9. package/server/lib/claude-bridge.ts +152 -3
  10. package/server/lib/claude-cli-support.ts +39 -0
  11. package/server/lib/claude-steer.ts +259 -0
  12. package/server/lib/env-switch.ts +17 -0
  13. package/server/lib/managed-restart.ts +8 -0
  14. package/server/lib/native-head.ts +65 -6
  15. package/server/lib/provider-actions.ts +18 -0
  16. package/server/lib/query-job-coordinator.ts +300 -0
  17. package/server/lib/query-job-feature.ts +39 -0
  18. package/server/lib/query-job-resume.ts +269 -0
  19. package/server/lib/query-job-runtime.ts +23 -3
  20. package/server/lib/query-job-store.ts +516 -7
  21. package/server/lib/query-job-types.ts +210 -1
  22. package/server/lib/queue-turn-evidence.ts +213 -0
  23. package/server/lib/thread-turn-queue-deliver.ts +6 -1
  24. package/server/lib/thread-turn-queue-store.ts +13 -4
  25. package/server/lib/thread-turn-queue.ts +151 -1
  26. package/server/routes/agent-session-bindings.ts +169 -11
  27. package/server/routes/health.ts +26 -1
  28. package/server/routes/maintenance.ts +34 -0
  29. package/server/routes/meeting.ts +27 -9
  30. package/server/routes/query-jobs.ts +107 -13
  31. package/server/routes/recovery.ts +4 -0
  32. package/server/routes/thread-turn-queue.ts +371 -10
  33. package/server/scripts/live-cues-v2-notes-mutation-gate.mjs +0 -170
  34. package/server/scripts/transcript-cleanup-mutation-gate.mjs +0 -219
@@ -1,170 +0,0 @@
1
- #!/usr/bin/env node
2
- // Live cues v2 6.76.2 mutation gate: the runtime switch, the notes fallback and the decision provider. Mutations run only
3
- // in a disposable copy of the checkout; the live checkout is never rewritten.
4
- //
5
- // What it proves, in order:
6
- // 0. The target suites are GREEN unmutated. A gate over a red suite reads every mutant as killed and proves nothing.
7
- // 1. Each target string appears EXACTLY ONCE in its file, so the mutation is unambiguous.
8
- // 2. The mutated file differs from the original, so the edit landed.
9
- // 3. A NAMED test fails ("FAIL server/x.test.ts > name"); a build error or a harness failure is not a kill.
10
- // 4. The file is restored byte for byte (compared) before the next mutant.
11
- // Also the Live COS Ask's memory bridge fallback (searchAskHistory) and the helpers it shares with cues (cos-bridge-notes.ts).
12
- // Usage: node server/scripts/live-cues-v2-notes-mutation-gate.mjs [--list] [name…]
13
- import { mkdtempSync, cpSync, readFileSync, writeFileSync, symlinkSync, rmSync } from 'node:fs'
14
- import { tmpdir } from 'node:os'
15
- import { resolve, join, dirname, basename } from 'node:path'
16
- import { fileURLToPath } from 'node:url'
17
- import { spawnSync } from 'node:child_process'
18
-
19
- const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
20
- const CUES = ['server/lib/live-cues-v2.test.ts', 'server/lib/live-cues-v2-notes.test.ts', 'server/lib/live-cues-v2-notes-bridge.test.ts',
21
- 'server/lib/live-cues-v2-settings.test.ts', 'server/routes/live-cues-v2-settings-route.test.ts', 'server/routes/live-cues-v2-route.test.ts']
22
- const HEALTH = [...CUES, 'server/routes/health.test.ts']
23
- const ASK = ['server/lib/live-cos-ask-memory-fallback.test.ts', 'server/lib/live-cos-ask-queen-runtime.test.ts', 'server/lib/meeting-ask-cos-search.test.ts',
24
- 'server/lib/live-cos-ask.test.ts', 'server/lib/meeting-ask-owner.test.ts']
25
- const BOTH = [...CUES, ...ASK]
26
- const ALL = [...HEALTH, ...ASK]
27
- const ENGINE = 'server/lib/live-cues-v2.ts', NOTES = 'server/lib/live-cues-v2-notes.ts', SETTINGS = 'server/lib/live-cues-v2-settings.ts'
28
- const ROUTE = 'server/routes/live-cues.ts', DECISION = 'server/lib/decision-provider.ts'
29
- const SHARED = 'server/lib/cos-bridge-notes.ts', ASKLIB = 'server/lib/live-cos-ask.ts'
30
- const m = (name, file, edits, tests = CUES) => ({ name, file, edits: typeof edits[0] === 'string' ? [edits] : edits, tests })
31
-
32
- const mutations = [
33
- // ── Runtime switch ──
34
- m('env-forces-only-exact-1', SETTINGS, ["return env.COS_LIVE_CUES_V2 === '1'", 'return !!env.COS_LIVE_CUES_V2']),
35
- m('env-forced-wins-over-setting', SETTINGS, ["if (liveCuesV2EnvForced(env)) return { enabled: true, source: 'env' }", 'if (false) return { enabled: true, source: \'env\' }']),
36
- m('saved-setting-read', SETTINGS, ["if (stored !== null) return { enabled: stored, source: 'setting' }", "if (stored === true) return { enabled: stored, source: 'setting' }"]),
37
- m('malformed-setting-is-none', SETTINGS, ["typeof (raw as { enabled?: unknown }).enabled === 'boolean'", '(raw as { enabled?: unknown }).enabled !== undefined']),
38
- m('cache-follows-write', SETTINGS, [' this.cache = { value: enabled }\n', '']),
39
- m('failed-write-keeps-memory', SETTINGS, [
40
- [' mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 })\n', ' this.cache = { value: enabled }\n mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 })\n'],
41
- ["{ mode: 0o600 })\n this.cache = { value: enabled }\n", '{ mode: 0o600 })\n']]),
42
- m('settings-file-private', SETTINGS, ["+ '\\n', { mode: 0o600 })", "+ '\\n', { mode: 0o644 })"]),
43
- m('engine-reads-setting', ENGINE, ['private enabled(): boolean { return liveCuesV2Enabled(this.deps.env, this.deps.settings) }', 'private enabled(): boolean { return liveCuesV2Enabled(this.deps.env, null) }']),
44
- m('feed-stops-while-off', ENGINE, [' if (!this.enabled()) return\n if (this.deps.store.isEnded(sessionId)) return', ' if (this.deps.store.isEnded(sessionId)) return']),
45
- m('last-check-before-emit', ENGINE, ['|| this.deps.store.isPaused(s.sessionId) || !this.enabled()) {', '|| this.deps.store.isPaused(s.sessionId)) {']),
46
- m('suspend-aborts-in-flight', ENGINE, ['if (s.inFlight) { s.inFlight.abort(V2_OFF); aborted += 1 }', 'if (s.inFlight) { aborted += 1 }']),
47
- m('cancel-reason-traced', ENGINE, [" if (trace.outcome === 'cancelled' && abort.signal.reason === V2_OFF) trace.cancelReason = V2_OFF\n", '']),
48
- m('env-forced-off-refused', ENGINE, ["if (!enabled && liveCuesV2EnvForced(env)) return { ok: false, error: 'env_forced' }", "if (false) return { ok: false, error: 'env_forced' }"]),
49
- m('turning-off-suspends', ENGINE, ['const aborted = engine?.suspend() ?? 0', 'const aborted = 0']),
50
- m('suspend-only-on-transition', ENGINE, ['if (wasOn && !liveCuesV2Enabled(env)) {', 'if (!liveCuesV2Enabled(env)) {']),
51
- m('saved-meeting-ends-known-session', ENGINE, ['(liveCuesV2Enabled() || engine?.knows(sid))', 'liveCuesV2Enabled()']),
52
- m('knows-armed-sessions', ENGINE, ['knows(sessionId: string): boolean { return this.sessions.has(sessionId) }', 'knows(sessionId: string): boolean { return false }']),
53
- m('route-v2-owns-known-meeting', ROUTE, ['return liveCuesV2Enabled() || (peekLiveCuesV2()?.knows(sessionId) ?? false)', 'return liveCuesV2Enabled()']),
54
- m('route-boolean-enabled-only', ROUTE, ["typeof body.enabled !== 'boolean'", 'body.enabled === undefined']),
55
- m('route-409-body', ROUTE, ['if (!result.ok) return res.status(409).json({ error: result.error })', 'if (!result.ok) return res.status(200).json(liveCuesV2SettingsView())']),
56
- m('advertised-follows-setting', ENGINE, ['return liveCuesV2Enabled(env) ? liveCuesV2Capability() : undefined', "return env.COS_LIVE_CUES_V2 === '1' ? liveCuesV2Capability() : undefined"], HEALTH),
57
- // ── Notes fallback (engine) ──
58
- m('fallback-runs-on-failure', ENGINE, ['if (cueMemoryFailed(mem) && !abort.signal.aborted) {', 'if (false) {']),
59
- m('cue-memory-preferred', ENGINE, ['if (cueMemoryFailed(mem) && !abort.signal.aborted) {', 'if (!abort.signal.aborted) {']),
60
- m('failure-codes-only', ENGINE, ['return !!mem.denseError && CUE_MEMORY_FAILURES.has(mem.denseError)', 'return !!mem.denseError']),
61
- m('timeout-is-a-failure', ENGINE, ["'no_cue_memory_script', 'memory_timeout', 'spawn_failed'", "'no_cue_memory_script', 'spawn_failed'"]),
62
- m('notes-candidates-to-judge', ENGINE, [' mem = { notes: found.notes }\n', ' mem = { notes: [] }\n']),
63
- m('notes-get-session-and-bound', ENGINE, ['this.deps.notes(window, before, abort.signal, s.sessionId)', "this.deps.notes(window, before, abort.signal, '')"]),
64
- m('source-none-when-empty', ENGINE, ["cueMemorySource: mem.notes.length ? source : 'none'", 'cueMemorySource: source']),
65
- m('capability-memory-source', ENGINE, [": notesAvailable() ? 'notes' : 'none'", ": 'none'"]),
66
- // ── Notes fallback (search) ──
67
- m('notes-date-bound', NOTES, ['!!c && !(c.date && c.date >= before)', '!!c']),
68
- m('notes-local-day', SHARED, ["return Number.isFinite(ms) ? new Date(ms).toLocaleDateString('en-CA') : ''", "return Number.isFinite(ms) ? v.slice(0, 10) : ''"], BOTH),
69
- m('notes-drop-live-meeting', NOTES, ['hits.filter(h => !(opts.sessionId && h.sessionId === opts.sessionId))', 'hits']),
70
- m('notes-budget-race', NOTES, ['new Promise<boolean>(r => { timer = setTimeout(() => r(true), budgetMs) })', 'new Promise<boolean>(() => {})']),
71
- m('notes-bridge-timeout', NOTES, ['searchers.bridge(local.signal, budgetMs)', 'searchers.bridge(local.signal, 30_000)']),
72
- m('notes-abort-at-budget', NOTES, [' clearTimeout(timer)\n local.abort()\n', ' clearTimeout(timer)\n']),
73
- m('notes-aborted-gets-nothing', NOTES, [' if (signal.aborted) return { notes: [] }\n', '']),
74
- m('notes-no-terms-no-search', NOTES, [" if (!terms.length) return { notes: [], error: 'notes_no_terms' }\n", '']),
75
- m('notes-queries-short', NOTES, ['terms.slice(0, 4).join', 'terms.join']),
76
- m('notes-stopwords', NOTES, ['if (term.length < 3 || STOP.has(term) ||', 'if (term.length < 3 ||']),
77
- m('shared-rank-fusion', SHARED, ['score: (prev?.score ?? 0) + 1 / (RRF_K + rank)', 'score: 1 / (RRF_K + rank)'], BOTH),
78
- m('shared-fusion-keeps-order-on-ties', SHARED, ['b.score - a.score || a.order - b.order', 'b.score - a.score || b.order - a.order'], BOTH),
79
- m('notes-top-four', NOTES, ['fused.ranked(top)', 'fused.ranked()']),
80
- m('shared-bridge-scored', SHARED, [' .filter(x => x.score > 0).sort(', ' .sort('], BOTH),
81
- m('shared-bridge-needs-content', SHARED, ['if (!id || !content.trim()) continue', 'if (!id) continue'], BOTH),
82
- m('shared-bridge-lists-memory', SHARED, ["callPython(['memory', '--days'", "callPython(['memory-overview', '--days'"], BOTH),
83
- m('notes-bridge-searched', NOTES, [' for (const q of queries) add(rankBridgeMemories(rows, q).map(bridgeCandidate))\n', '']),
84
- m('notes-real-memories-keyword-only', NOTES, ['(await searchMemories({ query, limit: 10, semantic: false, signal })).hits', '[]']),
85
- m('notes-real-meetings-keyword-only', NOTES, ['(await searchMeetingLibrary({ query, limit: 10, semantic: false, signal })).hits', '[]']),
86
- // ── Decision provider ──
87
- m('cue-override-read', ENGINE, [' if (raw !== undefined) {\n', ' if (false) {\n']),
88
- m('cue-override-validated', ENGINE, [" if (v !== 'jev' && v !== 'subq') throw new DecisionProviderError('decision_invalid_provider')\n", '']),
89
- m('only-key-subq', ENGINE, [" if (!keys.jev() && keys.subq()) return { provider: 'subq', source: 'only_key' }\n", '']),
90
- m('only-key-needs-no-jev', ENGINE, ['if (!keys.jev() && keys.subq()) return', 'if (keys.subq()) return']),
91
- m('global-env-ignored', ENGINE, [' const raw = env.COS_DECISION_PROVIDER_CUES\n', " const raw = env.COS_DECISION_PROVIDER_CUES ?? env.COS_DECISION_PROVIDER\n"]),
92
- m('global-setting-ignored', ENGINE, [
93
- ["import type { DecisionProvider } from './decision-settings.js'", "import { readDecisionSettings, type DecisionProvider } from './decision-settings.js'"],
94
- [' const raw = env.COS_DECISION_PROVIDER_CUES\n', ' const raw = env.COS_DECISION_PROVIDER_CUES ?? readDecisionSettings().provider\n']]),
95
- m('judge-uses-evaluation-provider', ENGINE, ['abort.signal, provider)\n Object.assign(trace, { judgeMs', 'abort.signal, this.deps.decisionProvider())\n Object.assign(trace, { judgeMs']),
96
- m('gate-uses-provider', ENGINE, ['await this.deps.gate(s.title, window, abort.signal, provider)', "await this.deps.gate(s.title, window, abort.signal, 'jev')"]),
97
- m('real-deps-route-subq', ENGINE, ["cueDecisionCalls(provider === 'subq' ? subq : jev)", 'cueDecisionCalls(jev)']),
98
- m('capability-key-check', ENGINE, [" if (!st.configured) return off(provider === 'jev' && this.deps.decisionState('subq').configured ? 'jev_not_configured' : 'decision_not_configured')\n", '']),
99
- m('jev-alias-only-with-subq-key', ENGINE, ["provider === 'jev' && this.deps.decisionState('subq').configured ? 'jev_not_configured'", "provider === 'jev' ? 'jev_not_configured'"]),
100
- m('decision-not-configured-reason', ENGINE, ["this.deps.decisionState('subq').configured ? 'jev_not_configured' : 'decision_not_configured'", "this.deps.decisionState('subq').configured ? 'jev_not_configured' : 'jev_not_configured'"]),
101
- m('capability-selected-provider-state', ENGINE, [' const st = this.deps.decisionState(provider)\n', " const st = this.deps.decisionState('jev')\n"]),
102
- m('subq-cap-reason', ENGINE, ["off(provider === 'jev' ? 'jev_cap_reached' : 'decision_cap_reached')", "off('jev_cap_reached')"]),
103
- m('subq-breaker-reason', ENGINE, ["off(provider === 'jev' ? 'jev_breaker_open' : 'decision_breaker_open')", "off('jev_breaker_open')"]),
104
- m('capability-names-provider', ENGINE, ['...(provider ? { decisionProvider: provider } : {})', '...({})']),
105
- m('quiet-decision-stops', ENGINE, ["'decision_cap_reached', 'decision_breaker_open', 'decision_not_configured', 'decision_invalid_provider'])", "'decision_breaker_open', 'decision_not_configured', 'decision_invalid_provider'])"]),
106
- m('quiet-sees-decision-errors', ENGINE, ['if ((error instanceof JevError || error instanceof DecisionProviderError) && QUIET_DECISION_STOPS.has(error.code)) {', 'if (error instanceof JevError && QUIET_DECISION_STOPS.has(error.code)) {']),
107
- m('decision-error-code-traced', ENGINE, ["trace.error = error instanceof JevError || error instanceof DecisionProviderError ? error.code :", "trace.error = error instanceof JevError ? error.code :"]),
108
- m('subq-cue-cap-env', ENGINE, ['Number(process.env.COS_LIVE_CUES_SUBQ_DAILY_TOKENS)', 'Number(process.env.COS_SUBQ_DAILY_TOKENS)']),
109
- m('subq-cue-ledger', ENGINE, ["dataPath('subq-cues-usage.json')", "dataPath('subq-usage.json')"]),
110
- m('subq-configured-honest', ENGINE, ['return { configured: st.configured, breakerOpen: st.breakerOpenUntil !== null', 'return { configured: true, breakerOpen: st.breakerOpenUntil !== null']),
111
- m('subq-status-unreadable-is-spent', DECISION, [' let usedToday = dailyCap\n', ' let usedToday = 0\n']),
112
- m('subq-tracked-for-new-key', DECISION, ['{ configuredSubqClients.add(client); return client }', '{ return client }']),
113
- // ── Live COS Ask: memory bridge fallback ──
114
- m('ask-listing-only-when-memory-did-not-answer', ASKLIB, [' const memoryDidNotAnswer = memoryRaw.length === 0 ||', ' const memoryDidNotAnswer = true ||'], ASK),
115
- m('ask-listing-when-memory-empty', ASKLIB, [' const memoryDidNotAnswer = memoryRaw.length === 0 ||', ' const memoryDidNotAnswer = false ||'], ASK),
116
- m('ask-listing-when-meaning-failed', ASKLIB, ["|| (memory.status === 'fulfilled' && semantic && !memory.value.semanticAvailable)\n", '\n'], ASK),
117
- m('ask-listing-meaning-only', ASKLIB, ["(memory.status === 'fulfilled' && semantic && !memory.value.semanticAvailable)", "(memory.status === 'fulfilled' && !memory.value.semanticAvailable)"], ASK),
118
- m('ask-listing-threshold', ASKLIB, ['x.score >= ASK_HIT_MIN && (!domain', 'x.score > 0 && (!domain'], ASK),
119
- m('ask-listing-domain', ASKLIB, ['x.score >= ASK_HIT_MIN && (!domain || x.row.domain === domain)', 'x.score >= ASK_HIT_MIN'], ASK),
120
- m('ask-memory-cap-three', ASKLIB, ['const memorySources = fused.ranked(3)', 'const memorySources = fused.ranked()'], ASK),
121
- m('ask-listing-notice', ASKLIB, [" if (memorySources.some(s => s.listed)) notices.push('COS also searched the notes in its memory list by keyword.')\n", ''], ASK),
122
- m('ask-abort-skips-listing', ASKLIB, ['if (memoryDidNotAnswer && !signal?.aborted) {', 'if (memoryDidNotAnswer) {'], ASK),
123
- m('ask-listing-failure-tolerated', ASKLIB, ['try { listed = await bridge(signal) } catch { /* no listing: the notices above stand */ }', 'listed = await bridge(signal)'], ASK),
124
- m('ask-source-id-first', ASKLIB, ["({ id: `H${sources.length + 1}`, ...s })", "({ ...s, id: `H${sources.length + 1}` })"], ASK),
125
- m('ask-listing-cites-content', ASKLIB, ['text: redactSecrets(row.content).slice(0, 1500), memoryId: row.id', "text: '', memoryId: row.id"], ASK),
126
- m('ask-default-listing-is-the-bridge', ASKLIB, ['bridge: AskBridgeList = s => listBridgeMemories(s)', 'bridge: AskBridgeList = async () => []'], ASK),
127
- ]
128
-
129
- const args = process.argv.slice(2)
130
- if (args.includes('--list')) { for (const { name } of mutations) console.log(name); process.exit(0) }
131
- const selected = args.length ? mutations.filter(({ name }) => args.includes(name)) : mutations
132
- if (args.length && selected.length !== args.length) throw new Error('Unknown mutation name in: ' + args.join(' '))
133
- if (new Set(mutations.map(x => x.name)).size !== mutations.length) throw new Error('Duplicate mutation name')
134
-
135
- const scratch = mkdtempSync(join(tmpdir(), 'cues-notes-mutations-'))
136
- try {
137
- const skip = new Set(['data', 'models', 'certs', 'node_modules'])
138
- cpSync(join(root, 'server'), join(scratch, 'server'), { recursive: true, filter: src => !(skip.has(basename(src)) && dirname(src) === join(root, 'server')) })
139
- for (const dir of ['shared', 'work-task-runtime', 'bin', 'docs', 'memory-runtime']) cpSync(join(root, dir), join(scratch, dir), { recursive: true })
140
- for (const file of ['vitest.config.ts', 'package.json', 'managed-runtime-contract.json']) cpSync(join(root, file), join(scratch, file))
141
- symlinkSync(join(root, 'node_modules'), join(scratch, 'node_modules'), 'dir')
142
- const run = tests => spawnSync(process.execPath, [join(root, 'node_modules/vitest/vitest.mjs'), 'run', '--maxWorkers=1', ...tests], {
143
- cwd: scratch, env: { PATH: process.env.PATH, HOME: scratch, TMPDIR: process.env.TMPDIR, COS_DATA_DIR: join(scratch, 'data'), COS_PROFILE_PATH: join(scratch, 'data', 'profile.json') },
144
- encoding: 'utf8', timeout: 300000,
145
- })
146
- const baseline = run(ALL)
147
- if (baseline.status !== 0) throw new Error('Baseline failed; a gate over a red suite proves nothing:\n' + baseline.stdout + '\n' + baseline.stderr)
148
- const totals = /Tests\s+(\d+) passed/.exec(baseline.stdout)
149
- console.log(`baseline PASS (${totals ? totals[1] : '?'} tests in ${ALL.length} files)`)
150
- let killed = 0
151
- for (const { name, file, edits, tests } of selected) {
152
- const target = join(scratch, file), original = readFileSync(target, 'utf8')
153
- let mutated = original
154
- for (const [find, replacement] of edits) {
155
- if (mutated.split(find).length !== 2) throw new Error(`Mutation ${name} does not match exactly once in ${file}: ${find.slice(0, 80)}`)
156
- mutated = mutated.replace(find, () => replacement)
157
- }
158
- if (mutated === original) throw new Error(`Mutation ${name} did not change ${file}`)
159
- writeFileSync(target, mutated)
160
- let result
161
- try { result = run(tests) } finally { writeFileSync(target, original) }
162
- if (readFileSync(target, 'utf8') !== original) throw new Error(`Restore failed for ${file} after ${name}`)
163
- const output = result.stdout + '\n' + result.stderr
164
- const killers = [...new Set([...output.matchAll(/FAIL\s+(server\/\S+\.test\.ts > [^\n]+)/g)].map(x => x[1].trim()))]
165
- if (result.status === 0 || result.error || !killers.length) { console.log(`${name} SURVIVED or harness failed\n${output.slice(-3000)}`); process.exitCode = 1; continue }
166
- killed += 1
167
- console.log(`${name} KILLED by ${killers.slice(0, 2).join(' | ')}${killers.length > 2 ? ` (+${killers.length - 2} more)` : ''}`)
168
- }
169
- console.log(`${killed} of ${selected.length} live cues and Ask 6.76.2 mutations killed`)
170
- } finally { rmSync(scratch, { recursive: true, force: true }) }
@@ -1,219 +0,0 @@
1
- #!/usr/bin/env node
2
- // Transcript cleanup 6.77.0 mutation gate: span selection, the deterministic fixes, the edit guards, the ledger, the stop
3
- // conditions, the screen-context store and route, the dictionary suggestions and the save wiring. Mutations run only in a
4
- // disposable copy of the checkout; the live checkout is never rewritten.
5
- //
6
- // What it proves, in order:
7
- // 0. The target suites are GREEN unmutated. A gate over a red suite reads every mutant as killed and proves nothing.
8
- // 1. Each target string appears EXACTLY ONCE in its file, so the mutation is unambiguous.
9
- // 2. The mutated file differs from the original, so the edit landed.
10
- // 3. A NAMED test fails ("FAIL server/x.test.ts > name"); a build error or a harness failure is not a kill.
11
- // 4. The file is restored byte for byte (compared) before the next mutant.
12
- // Usage: node server/scripts/transcript-cleanup-mutation-gate.mjs [--list | --check] [name…]
13
- import { mkdtempSync, cpSync, readFileSync, writeFileSync, symlinkSync, rmSync } from 'node:fs'
14
- import { tmpdir } from 'node:os'
15
- import { resolve, join, dirname, basename } from 'node:path'
16
- import { fileURLToPath } from 'node:url'
17
- import { spawnSync } from 'node:child_process'
18
-
19
- const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
20
- const LIB = ['server/lib/transcript-cleanup.test.ts', 'server/lib/transcript-cleanup-spans.test.ts']
21
- const ROUTE = ['server/routes/transcript-cleanup.test.ts']
22
- const MEETING = ['server/routes/meeting.test.ts']
23
- const WIRE = ['server/routes/health.test.ts', 'server/routes/transcribe.test.ts', 'server/lib/transcript-cleanup-provider.test.ts']
24
- const ALL = [...LIB, ...ROUTE, ...MEETING, ...WIRE]
25
- const RUN = 'server/lib/transcript-cleanup.ts', SPANS = 'server/lib/transcript-cleanup-spans.ts', TEXT = 'server/lib/transcript-cleanup-text.ts'
26
- const MODEL = 'server/lib/transcript-cleanup-model.ts', LEDGER = 'server/lib/transcript-cleanup-ledger.ts', STATE = 'server/lib/transcript-cleanup-state.ts'
27
- const SCREEN = 'server/lib/transcript-cleanup-screen.ts', SUG = 'server/lib/transcript-cleanup-suggestions.ts', CTX = 'server/lib/transcript-cleanup-context.ts'
28
- const R = 'server/routes/transcript-cleanup.ts', MEET = 'server/routes/meeting.ts'
29
- const m = (name, file, edits, tests = LIB) => ({ name, file, edits: typeof edits[0] === 'string' ? [edits] : edits, tests })
30
-
31
- const mutations = [
32
- // ── Switch, model, stops ──
33
- m('switch-checked-first', RUN, ['if (!resolveCleanupSwitch(deps.env, deps.settings).enabled) return', 'if (false) return']),
34
- m('env-0-forces-off', STATE, [" if (env.COS_TRANSCRIPT_CLEANUP === '0') return { enabled: false, source: 'env' }\n", '']),
35
- m('env-1-forces-on', STATE, [" if (env.COS_TRANSCRIPT_CLEANUP === '1') return { enabled: true, source: 'env' }\n", '']),
36
- m('default-off', STATE, [" return { enabled: false, source: 'default' }", " return { enabled: true, source: 'default' }"], [...LIB, ...ROUTE]),
37
- m('default-model-haiku', STATE, ["return { choice: 'haiku', id: CLEANUP_MODEL_IDS.haiku, source: 'default' }", "return { choice: 'sonnet', id: CLEANUP_MODEL_IDS.sonnet, source: 'default' }"], [...LIB, ...ROUTE]),
38
- m('setting-model-read', STATE, [" if (stored) return { choice: stored, id: CLEANUP_MODEL_IDS[stored], source: 'setting' }\n", '']),
39
- m('env-model-wins', STATE, [" if (raw === 'haiku' || raw === 'sonnet') return { choice: raw, id: CLEANUP_MODEL_IDS[raw], source: 'env' }\n", '']),
40
- m('charge-before-call', RUN, ["else if (!deps.budget.charge(limits)) modelSkipped = 'daily_calls'", "else if (false) modelSkipped = 'daily_calls'"]),
41
- m('gate-breaker', STATE, [" if (s.breakerOpenUntil !== null && this.now().getTime() < s.breakerOpenUntil) return 'breaker_open'\n", '']),
42
- m('gate-daily-calls', STATE, [" if (s.calls >= limits.dailyCalls) return 'daily_calls'\n", '']),
43
- m('gate-daily-tokens', STATE, [" if (s.tokens >= limits.dailyTokens) return 'daily_tokens'\n", '']),
44
- m('usage-recorded', RUN, [' deps.budget.recordUsage(trace.inputTokens + trace.outputTokens)\n', '']),
45
- m('provider-limit-opens-breaker', STATE, ['const open = providerLimit || failures >= limits.breakerFailures', 'const open = failures >= limits.breakerFailures']),
46
- m('breaker-after-three', STATE, [' breakerFailures: 3,', ' breakerFailures: 4,']),
47
- m('success-closes-breaker', STATE, [' this.write({ ...s, consecutiveFailures: 0, breakerOpenUntil: null })', ' return']),
48
- m('unreadable-state-is-spent', STATE, [' } catch {\n return spent\n', ' } catch {\n return { day, calls: 0, tokens: 0, consecutiveFailures: 0, breakerOpenUntil: null }\n']),
49
- m('future-day-is-spent', STATE, [' if (raw.day > day) return { ...spent, ...breaker }\n', '']),
50
- m('negative-count-is-spent', STATE, [' if (!(calls >= 0) || !(tokens >= 0)) return { ...spent, ...breaker }\n', '']),
51
- m('failure-recorded', RUN, [' deps.budget.recordFailure(limits, limit)\n', '']),
52
- m('api-key-guard', RUN, ["else if (deps.env.ANTHROPIC_API_KEY && deps.env.COS_ALLOW_API_FALLBACK !== '1') modelSkipped = 'api_key_in_env'", "else if (false) modelSkipped = 'api_key_in_env'"]),
53
- m('claude-cli-guard', RUN, ["else if (!deps.claudeAvailable()) modelSkipped = 'claude_not_found'", "else if (false) modelSkipped = 'claude_not_found'"]),
54
- m('live-session-skipped', RUN, [" if (live) return finish({ status: 'skipped', reason: 'session_live', applied: 0 })", " if (false) return finish({ status: 'skipped', reason: 'session_live', applied: 0 })"]),
55
- m('span-cap-per-meeting', RUN, ['{ maxSpans: limits.maxSpans, fixed:', '{ maxSpans: 80, fixed:']),
56
- m('idempotent-rerun', RUN, [" if (alreadyCleaned(input.sessionId, inputSha)) return finish({ status: 'skipped', reason: 'already_done', applied: 0 })", " if (false) return finish({ status: 'skipped', reason: 'already_done', applied: 0 })"]),
57
- m('ledger-intent-first', RUN, [" if (!appendLedger(input.sessionId, [{ kind: 'run', runId, phase: 'intent', at: at(), inputSha }])) {", " if (!appendLedger(input.sessionId, [{ kind: 'run', runId, phase: 'intent', at: at(), inputSha }]) && false) {"]),
58
- m('prompt-size-cap', MODEL, [' if (prompt.length + block.length + 1 > MODEL_LIMITS.promptChars) break\n', '']),
59
- m('screen-lines-in-prompt', MODEL, ['const screen = screenLinesFor(pack.screen, span.atMs, span.near, { max: MODEL_LIMITS.screenLinesPerSpan })', 'const screen: typeof pack.screen = []']),
60
- m('owner-in-prompt', MODEL, [' if (pack.owner) {\n head.push', ' if (false) {\n head.push']),
61
- m('parse-scans-past-first-object', MODEL, [" i = text.indexOf('{', i + 1)", ' i = -1']),
62
- m('parse-string-aware', MODEL, [" else if (c === '\"') inString = false", '']),
63
-
64
- m('notes-exclude-this-meeting', RUN, ['if (!hit.recordId || hit.sessionId === sessionId) continue', 'if (!hit.recordId) continue']),
65
- m('notes-bridge-when-memory-empty', RUN, [' if (!memoryHits && !signal.aborted) {', ' if (!signal.aborted) {']),
66
- m('notes-in-prompt', MODEL, [' for (const n of pack.notes) head.push', ' for (const n of [] as typeof pack.notes) head.push']),
67
- m('notes-snippet-bounded', CTX, [
68
- ["snippet: redactSecrets(n.snippet).replace(/\\s+/g, ' ').slice(0, CONTEXT_LIMITS.noteChars) }\n pack.notes.push(note)", "snippet: n.snippet }\n pack.notes.push(note)"]]),
69
- m('notes-snippet-clipped-in-prompt', MODEL, ['${quote(n.title)}: ${clip(n.snippet, 280)}`)', '${quote(n.title)}: ${n.snippet}`)']),
70
-
71
- // ── Span selection ──
72
- m('parakeet-floor', SPANS, [" if (engine.startsWith('parakeet')) return LOW_CONFIDENCE.parakeet\n", '']),
73
- m('fillers-never-doubtful', SPANS, [' && !FILLERS.has(t.key)) flag(t.i, ', ') flag(t.i, ']),
74
- m('near-name-needs-unsure', SPANS, ['const unsure = input.probability === undefined || input.probability < NEAR_NAME_UNSURE', 'const unsure = true']),
75
- m('near-name-inflection-skip', SPANS, [' if (inflectionOf(key, t.key)) continue\n', '']),
76
- m('exact-join-is-a-hit', SPANS, [' if (d === 0 && !input.join) return null', ' if (d === 0) return null']),
77
- m('join-no-function-words', SPANS, ['group.some(g => g.key.length > 5 || FUNCTION_WORDS.has(g.key))', 'group.some(g => g.key.length > 5)']),
78
- m('join-pieces-short', SPANS, ['group.some(g => g.key.length > 5 || FUNCTION_WORDS.has(g.key))', 'group.some(g => FUNCTION_WORDS.has(g.key))']),
79
- m('sounds-alike-onset', TEXT, [' if (vowelOnset(fa) !== vowelOnset(fb)) return false\n', '']),
80
- m('sounds-alike-two-consonants', TEXT, [' if (sa.length < 2 || sa !== sb) return false', ' if (sa !== sb) return false']),
81
- m('oov-not-at-sentence-start', SPANS, ['if (fixed.has(t.i) || t.sentenceStart || !/^\\p{Lu}/u.test(t.core)', 'if (fixed.has(t.i) || !/^\\p{Lu}/u.test(t.core)']),
82
- m('repeat-needs-three', SPANS, ['if (reps >= (unit === 1 ? 4 : 3)) found', 'if (reps >= 2) found']),
83
- m('merge-stops-at-clause', SPANS, [' if (tokens.slice(to - 1, next).some(t => /[,.;:!?]["\')\\]]*$/.test(t.raw))) break\n', '']),
84
- m('merge-stays-in-line', SPANS, [' if (tokens[next].line !== tokens[from].line || next - to > 1', ' if (next - to > 1']),
85
- m('span-word-cap', SPANS, [' maxSpanWords: 6,', ' maxSpanWords: 8,']),
86
- m('names-ranked-first', SPANS, ["(d.reasons.has('near_name') ? 4 : 0)", "(d.reasons.has('near_name') ? 0 : 0)"]),
87
- m('align-by-folded-word', TEXT, ['if (words[k].word === token.raw || fold(words[k].word) === token.key)', 'if (words[k].word === token.raw)']),
88
- m('word-time-on-meeting-clock', TEXT, ['Math.round(startTime + startElapsed + s * 1000)', 'Math.round(startTime + s * 1000)']),
89
-
90
- // ── Deterministic fixes ──
91
- m('fix-capitalised-only', SPANS, ['if (!/^\\p{Lu}/u.test(base) || allLower.has(base.toLowerCase()) || known.has(fold(base))) continue', 'if (allLower.has(base.toLowerCase()) || known.has(fold(base))) continue']),
92
- m('fix-never-a-known-spelling', SPANS, ['if (!/^\\p{Lu}/u.test(base) || allLower.has(base.toLowerCase()) || known.has(fold(base))) continue', 'if (!/^\\p{Lu}/u.test(base) || allLower.has(base.toLowerCase())) continue']),
93
- m('fix-strong-only-when-unsure', SPANS, ['let replacement = unsure ? fuzzyReplacementFor(base, strong, allLower) : null', 'let replacement = fuzzyReplacementFor(base, strong, allLower)']),
94
- m('fix-sentence-opener-only-unsure', SPANS, [' if (token.sentenceStart && !unsure) continue\n', '']),
95
- m('fix-no-inflection', SPANS, ['if (replacement === null || replacement === base || inflectionOf(base, replacement)) continue', 'if (replacement === null || replacement === base) continue']),
96
- m('fix-no-attendee-targets', RUN, ['const strongTargets = [...pack.vocabulary, ...pack.corrections.map(c => c.written), ...(pack.owner ? [pack.owner.name] : [])]', 'const strongTargets = [...pack.vocabulary, ...pack.corrections.map(c => c.written), ...pack.speakers, ...(pack.owner ? [pack.owner.name] : [])]']),
97
-
98
- // ── Edit guards ──
99
- m('only-sent-ids', MODEL, ['const byId = new Map(spans.filter(s => sentIds.includes(s.id)).map(s => [s.id, s]))', 'const byId = new Map(spans.map(s => [s.id, s]))']),
100
- m('duplicate-rejected', MODEL, [" if (seen.has(id)) { reject('duplicate'); continue }\n", '']),
101
- m('bracket-is-unsafe', MODEL, ["/[\\u0000-\\u001f\\u007f\\[\\]]/.test(edit!.text", "/[\\u0000-\\u001f\\u007f]/.test(edit!.text"]),
102
- m('no-op-rejected', MODEL, [" if (text === span.text) { reject('no_op'); continue }\n", '']),
103
- m('word-count-cap', MODEL, ['newWords.length > spanWords.length + 1 ||', 'newWords.length > spanWords.length + 9 ||']),
104
- m('char-length-cap', MODEL, [' || text.length > span.text.length * 2 + 8) {', ') {']),
105
- m('repeat-shrinks-to-own-run', MODEL, [" if (!isRun) { reject('longer_than_span'); continue }\n", '']),
106
- m('outside-span-before', MODEL, ['(beforeLast && firstNew === beforeLast && !spanKeys.has(beforeLast)) || ', '']),
107
- m('outside-span-after', MODEL, [' || (afterFirst && lastNew === afterFirst && !spanKeys.has(afterFirst))', '']),
108
- m('numbers-unchanged', MODEL, [" if (numbers(text) !== numbers(span.text)) { reject('number_unit'); continue }\n", '']),
109
- m('negations-unchanged', MODEL, [" if (negationCount(text) !== negationCount(span.text)) { reject('negation'); continue }\n", '']),
110
- m('added-content-rejected', MODEL, [" if (unsupported.length) { reject('added_content'); continue }\n", '']),
111
- m('context-terms-admit', MODEL, ['const knownTerm = (w: string) => pack.terms.has(fold(w))', 'const knownTerm = (_w: string) => false']),
112
-
113
- // ── Ledger and writes ──
114
- m('overlap-dropped', LEDGER, [' if (e.start < cursor || e.end > source.length || e.start >= e.end) continue', ' if (e.end > source.length || e.start >= e.end) continue']),
115
- m('commit-refuses-changed-files', LEDGER, [" if (markdownNow !== options.markdown || sidecarNow !== options.sidecarText) throw new CleanupWriteError('changed_during_cleanup')\n", '']),
116
- m('failed-markdown-restores-sidecar', LEDGER, [' try { write(options.sidecarPath, options.sidecarText) } catch { /* the two files now disagree; say so */ }\n', '']),
117
- m('batch-transcript-kept-canonical', LEDGER, ["sidecar.batchTranscript.replace(/\\n+$/, '') === oldSection) sidecar.batchTranscript = options.transcript", "sidecar.batchTranscript.replace(/\\n+$/, '') === oldSection) sidecar.batchTranscript = sidecar.batchTranscript"]),
118
- m('lifecycle-revision-bumped', LEDGER, [' sidecar.lifecycleRevision = Number(sidecar.lifecycleRevision ?? 0) + 1\n', '']),
119
- m('undo-refuses-stale', LEDGER, [" if (at < 0) return { ok: false, status: 409, error: 'stale' }\n", '']),
120
- m('undo-exact-offsets', LEDGER, [' if (untouched.get(line.index) && line.text.slice(edit.offset, edit.offset + edit.replacement.length) === edit.replacement) at = edit.offset', ' if (false) at = edit.offset']),
121
- m('undo-records-revert', LEDGER, [" appendLedger(options.sessionId, targets.map(t => ({ kind: 'revert' as const, editId: t.id, at: now })))\n", '']),
122
-
123
- // ── Screen context ──
124
- m('screen-secrets-held', SCREEN, [' if (imageTextHasSecrets(o.text) || imageTextHasSecrets(text)) { result.held++; continue }\n', ''], [...LIB, ...ROUTE]),
125
- m('screen-line-truncated', SCREEN, [' if (text.length > SCREEN_LIMITS.charsPerLine) { text = text.slice(0, SCREEN_LIMITS.charsPerLine); result.truncated++ }\n', '']),
126
- m('screen-duplicates', SCREEN, [" if (recent.some(r => norm(r.text) === key && Math.abs(r.at - at) <= SCREEN_LIMITS.duplicateWindowMs)) { result.duplicates++; continue }\n", '']),
127
- m('screen-clock-bounds', SCREEN, ['at === null || at < earliest || at > latest ||', 'at === null ||']),
128
- m('screen-meeting-line-cap', SCREEN, ['if (result.stored >= SCREEN_LIMITS.linesPerMeeting || storedBytes', 'if (storedBytes']),
129
- m('screen-request-line-cap', SCREEN, [" if (lines.length > SCREEN_LIMITS.linesPerRequest) throw new ScreenContextError('too_many_lines', 413)\n", ''], [...LIB, ...ROUTE]),
130
- m('screen-request-byte-cap', SCREEN, [" if (requestBytes > SCREEN_LIMITS.bytesPerRequest) throw new ScreenContextError('request_too_large', 413)\n", ''], [...LIB, ...ROUTE]),
131
- m('screen-path-confined', SCREEN, [
132
- [' if (!SESSION_ID.test(sessionId)) return null\n', ''],
133
- [" return resolve(path).startsWith(resolve(dir) + '/') ? path : null", ' return path']]),
134
- m('route-unknown-session-404', R, [" if (!meeting) return { ok: false, status: 404, error: 'unknown_session' }\n", ''], ROUTE),
135
- m('route-cleanup-done-409', R, [" return done ? { ok: false, status: 409, error: 'cleanup_done' } : { ok: true }", ' return { ok: true }'], ROUTE),
136
- m('route-env-forced-409', R, [" if (hasEnabled && (env.COS_TRANSCRIPT_CLEANUP === '1' || env.COS_TRANSCRIPT_CLEANUP === '0') && body.enabled !== (env.COS_TRANSCRIPT_CLEANUP === '1')) {", ' if (false) {'], ROUTE),
137
- m('sweep-keeps-known', SCREEN, [' try { gone = !opts.known(sessionId) } catch { gone = false }', ' gone = true'], ROUTE),
138
- m('sweep-screen-retention', SCREEN, [" const expired = kind === 'screen' && age > retentionMs", ' const expired = false'], ROUTE),
139
-
140
- // ── Dictionary suggestions ──
141
- m('suggestion-needs-evidence', SUG, ['if (!written || written.length > 100 || !input.evidence.length) return null', 'if (!written || written.length > 100) return null'], [...LIB, ...ROUTE]),
142
- m('suggestion-not-for-dictionary-words', SUG, [' if (input.inDictionary(written)) return null\n', ''], ROUTE),
143
- m('suggestion-dismissed-stays-out', SUG, [" if (existing && existing.status !== 'pending') return null\n", ''], ROUTE),
144
- m('suggestion-dedupe-case', SUG, ['const existing = list.find(s => same(s.written, written))', 'const existing = list.find(s => s.written === written)'], ROUTE),
145
- m('orchestrator-skips-dictionary-terms', RUN, [
146
- [' if (!term || inDictionary(term) || heardKeys.has(term.key)) continue', ' if (!term || heardKeys.has(term.key)) continue'],
147
- [" if (deps.suggestions.record({ written: term.display, heard: edit.original, evidence, sessionId, at, inDictionary: w => dictionary.has(w.trim().toLowerCase()) })) recorded++", " if (deps.suggestions.record({ written: term.display, heard: edit.original, evidence, sessionId, at, inDictionary: () => false })) recorded++"]]),
148
- m('orchestrator-longest-term', RUN, [' if (parts.every(p => covered.has(p))) continue\n', '']),
149
- m('evidence-maps-screen', CTX, ["['screen', 'screen'], ", '']),
150
- m('accept-uses-vocabulary-writer', R, [' : await addVocabulary(suggestion.written)', " : { status: 200, body: { unchanged: false, glossary: null } }"], ROUTE),
151
- m('accept-correction-path', R, ["const outcome = body.correction === true && heard", 'const outcome = false && heard'], ROUTE),
152
- m('accept-refuses-dismissed', R, [" if (suggestion.status === 'dismissed') return res.status(409).json({ error: 'suggestion_dismissed' })\n", ''], ROUTE),
153
-
154
- // ── Save wiring and capability ──
155
- m('cleanup-stage-in-finalization', MEET, [' await runCleanupStage(runtime.transcriptCleanup, { sessionId: current.sessionId, meetingPath: current.meetingPath, sidecarPath: current.sidecarPath }, runtime.sessions)\n', ''], MEETING),
156
- m('cleanup-stage-never-fails-save', MEET, [' try {\n await run(input)\n } catch (error) {', ' {\n await run(input)\n } if (false) { const error = null as unknown;'], MEETING),
157
- m('health-says-off', R, [' return { protocol: TRANSCRIPT_CLEANUP_PROTOCOL, enabled }', ' return { protocol: TRANSCRIPT_CLEANUP_PROTOCOL, enabled: true }'], [...ROUTE, ...WIRE]),
158
- m('capabilities-advertised', 'server/routes/transcribe.ts', [' transcriptCleanup: { protocol: 1, suggestions: 1, screenContext: 1 },\n', ''], WIRE),
159
- m('provider-source-tag', RUN, ["source: 'transcript-cleanup', maxOutputTokens: opts.maxOutputTokens", "source: 'lens_gist', maxOutputTokens: opts.maxOutputTokens"], WIRE),
160
- ]
161
-
162
- const args = process.argv.slice(2)
163
- if (args.includes('--list')) { for (const { name } of mutations) console.log(name); process.exit(0) }
164
- const selected = args.length && !args.includes('--check') ? mutations.filter(({ name }) => args.includes(name)) : mutations
165
- if (args.length && !args.includes('--check') && selected.length !== args.length) throw new Error('Unknown mutation name in: ' + args.join(' '))
166
- if (new Set(mutations.map(x => x.name)).size !== mutations.length) throw new Error('Duplicate mutation name')
167
- // --check: every target matches exactly once and changes its file. Runs no tests.
168
- if (args.includes('--check')) {
169
- let bad = 0
170
- for (const { name, file, edits } of mutations) {
171
- let text = readFileSync(join(root, file), 'utf8')
172
- const original = text
173
- for (const [find, replacement] of edits) {
174
- const n = text.split(find).length - 1
175
- if (n !== 1) { console.log(`${name}: matches ${n} times in ${file}: ${find.slice(0, 90)}`); bad++ }
176
- else text = text.replace(find, () => replacement)
177
- }
178
- if (text === original) { console.log(`${name}: no change`); bad++ }
179
- }
180
- console.log(`${mutations.length - bad} of ${mutations.length} targets match exactly once`)
181
- process.exit(bad ? 1 : 0)
182
- }
183
-
184
- const scratch = mkdtempSync(join(tmpdir(), 'transcript-cleanup-mutations-'))
185
- try {
186
- const skip = new Set(['data', 'models', 'certs', 'node_modules'])
187
- cpSync(join(root, 'server'), join(scratch, 'server'), { recursive: true, filter: src => !(skip.has(basename(src)) && dirname(src) === join(root, 'server')) })
188
- for (const dir of ['shared', 'work-task-runtime', 'bin', 'docs']) cpSync(join(root, dir), join(scratch, dir), { recursive: true })
189
- for (const file of ['vitest.config.ts', 'package.json', 'managed-runtime-contract.json']) cpSync(join(root, file), join(scratch, file))
190
- symlinkSync(join(root, 'node_modules'), join(scratch, 'node_modules'), 'dir')
191
- const run = tests => spawnSync(process.execPath, [join(root, 'node_modules/vitest/vitest.mjs'), 'run', '--maxWorkers=1', ...tests], {
192
- cwd: scratch, env: { PATH: process.env.PATH, HOME: scratch, TMPDIR: process.env.TMPDIR, COS_DATA_DIR: join(scratch, 'data'), COS_PROFILE_PATH: join(scratch, 'data', 'profile.json') },
193
- encoding: 'utf8', timeout: 300000,
194
- })
195
- const baseline = run(ALL)
196
- if (baseline.status !== 0) throw new Error('Baseline failed; a gate over a red suite proves nothing:\n' + baseline.stdout + '\n' + baseline.stderr)
197
- const totals = /Tests\s+(\d+) passed/.exec(baseline.stdout)
198
- console.log(`baseline PASS (${totals ? totals[1] : '?'} tests in ${ALL.length} files)`)
199
- let killed = 0
200
- for (const { name, file, edits, tests } of selected) {
201
- const target = join(scratch, file), original = readFileSync(target, 'utf8')
202
- let mutated = original
203
- for (const [find, replacement] of edits) {
204
- if (mutated.split(find).length !== 2) throw new Error(`Mutation ${name} does not match exactly once in ${file}: ${find.slice(0, 80)}`)
205
- mutated = mutated.replace(find, () => replacement)
206
- }
207
- if (mutated === original) throw new Error(`Mutation ${name} did not change ${file}`)
208
- writeFileSync(target, mutated)
209
- let result
210
- try { result = run(tests) } finally { writeFileSync(target, original) }
211
- if (readFileSync(target, 'utf8') !== original) throw new Error(`Restore failed for ${file} after ${name}`)
212
- const output = result.stdout + '\n' + result.stderr
213
- const killers = [...new Set([...output.matchAll(/FAIL\s+(server\/\S+\.test\.ts > [^\n]+)/g)].map(x => x[1].trim()))]
214
- if (result.status === 0 || result.error || !killers.length) { console.log(`${name} SURVIVED or harness failed\n${output.slice(-3000)}`); process.exitCode = 1; continue }
215
- killed += 1
216
- console.log(`${name} KILLED by ${killers.slice(0, 2).join(' | ')}${killers.length > 2 ? ` (+${killers.length - 2} more)` : ''}`)
217
- }
218
- console.log(`${killed} of ${selected.length} transcript cleanup 6.77.0 mutations killed`)
219
- } finally { rmSync(scratch, { recursive: true, force: true }) }