instar 1.3.736 → 1.3.738
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/dist/commands/devPreflight.d.ts +21 -0
- package/dist/commands/devPreflight.d.ts.map +1 -1
- package/dist/commands/devPreflight.js +36 -2
- package/dist/commands/devPreflight.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +30 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +13 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +12 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +12 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +22 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/hostSemaphoreCore.d.ts +115 -0
- package/dist/core/hostSemaphoreCore.d.ts.map +1 -0
- package/dist/core/hostSemaphoreCore.js +215 -0
- package/dist/core/hostSemaphoreCore.js.map +1 -0
- package/dist/core/hostSpawnSemaphore.d.ts +8 -9
- package/dist/core/hostSpawnSemaphore.d.ts.map +1 -1
- package/dist/core/hostSpawnSemaphore.js +48 -126
- package/dist/core/hostSpawnSemaphore.js.map +1 -1
- package/dist/core/hostTestRunnerSemaphore.d.ts +492 -0
- package/dist/core/hostTestRunnerSemaphore.d.ts.map +1 -0
- package/dist/core/hostTestRunnerSemaphore.js +1648 -0
- package/dist/core/hostTestRunnerSemaphore.js.map +1 -0
- package/dist/core/promptClauses.d.ts +110 -0
- package/dist/core/promptClauses.d.ts.map +1 -0
- package/dist/core/promptClauses.js +125 -0
- package/dist/core/promptClauses.js.map +1 -0
- package/dist/core/testRunnerRunClassifier.d.ts +128 -0
- package/dist/core/testRunnerRunClassifier.d.ts.map +1 -0
- package/dist/core/testRunnerRunClassifier.js +382 -0
- package/dist/core/testRunnerRunClassifier.js.map +1 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts +4 -0
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +72 -0
- package/dist/data/llmBenchCoverage.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +26 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +9 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +150 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lib/test-runner-selfdisable-patterns.mjs +326 -0
- package/scripts/pre-push-test-runner-selfdisable.mjs +81 -0
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/llmBenchCoverage.ts +111 -0
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.737.md +140 -0
- package/upgrades/1.3.738.md +73 -0
- package/upgrades/side-effects/authority-clause-standard.md +67 -0
- package/upgrades/side-effects/test-runner-concurrency-bound.md +173 -0
- package/upgrades/1.3.736.md +0 -57
package/package.json
CHANGED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-runner-selfdisable-patterns.mjs — the SHARED serverless-host
|
|
3
|
+
* self-disable pattern detector for the test-runner concurrency bound
|
|
4
|
+
* (docs/specs/test-runner-concurrency-bound.md §2.6(b), §2.9, §5 last bullet).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS LIVES IN scripts/lib AS PLAIN ESM (one implementation, two
|
|
7
|
+
* consumers): the pre-push hook chain (scripts/pre-push-*.mjs) deliberately
|
|
8
|
+
* imports nothing from src/ or dist/ — a stale or missing dist must never
|
|
9
|
+
* break (let alone block) a push — while `instar dev:preflight`
|
|
10
|
+
* (src/commands/devPreflight.ts) runs its checks by SPAWNING commands.
|
|
11
|
+
* Implementing the detector once here and having dev:preflight spawn the same
|
|
12
|
+
* consumer script (scripts/pre-push-test-runner-selfdisable.mjs --preflight)
|
|
13
|
+
* keeps a single implementation reachable from both module systems.
|
|
14
|
+
*
|
|
15
|
+
* The paths + event shapes read here are the FROZEN rendezvous contract of
|
|
16
|
+
* spec §4 (canonical TS implementation: src/core/hostTestRunnerSemaphore.ts —
|
|
17
|
+
* resolveTestRunnerPaths / readLedgerTail / listLedgerSegments). This module
|
|
18
|
+
* re-reads the same frozen paths directly instead of importing the compiled
|
|
19
|
+
* module, per the dist-independence requirement above; the unit suite
|
|
20
|
+
* (tests/unit/test-runner-selfdisable-patterns.test.ts) pins the shared
|
|
21
|
+
* contract.
|
|
22
|
+
*
|
|
23
|
+
* ADVISORY-ONLY BY DESIGN: every read is best-effort and torn/malformed-line
|
|
24
|
+
* TOLERANT — a missing, unreadable, or garbage ledger yields ZERO findings
|
|
25
|
+
* and never throws out of the public entry points. Spec §2.6: "ledger content
|
|
26
|
+
* may only ever ADD a warning, never block a push."
|
|
27
|
+
*
|
|
28
|
+
* "SUSTAINED" (the spec leaves the threshold to the builder — documented
|
|
29
|
+
* choice): a pattern is flagged when >= DEFAULT_THRESHOLD (3) matching
|
|
30
|
+
* events land within DEFAULT_WINDOW_MS (48h) among the newest
|
|
31
|
+
* DEFAULT_MAX_EVENTS (400) ledger events (live file + newest 2 rotated
|
|
32
|
+
* segments). Rationale: one skip is a deliberate one-off, two may be a
|
|
33
|
+
* retry; three within two days is a pattern worth a WARN — and the surface
|
|
34
|
+
* is advisory-only, so a false positive costs one stderr line, never a
|
|
35
|
+
* blocked push.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_THRESHOLD = 3;
|
|
43
|
+
export const DEFAULT_WINDOW_MS = 48 * 60 * 60 * 1000; // 48h
|
|
44
|
+
export const DEFAULT_MAX_EVENTS = 400;
|
|
45
|
+
/** Newest rotated segments consulted in addition to the live ledger file. */
|
|
46
|
+
export const MAX_SEGMENTS_READ = 2;
|
|
47
|
+
|
|
48
|
+
// Code-default caps + pinned sanity ceilings (spec §2.9 — mirrors
|
|
49
|
+
// HOST_TEST_SUITE_CAP_DEFAULT/…CEILING in src/core/hostTestRunnerSemaphore.ts;
|
|
50
|
+
// pinned by the shared-contract unit test).
|
|
51
|
+
export const SUITE_CAP_DEFAULT = 1;
|
|
52
|
+
export const TARGETED_CAP_DEFAULT = 6;
|
|
53
|
+
export const SUITE_CAP_CEILING = 4;
|
|
54
|
+
export const TARGETED_CAP_CEILING = 24;
|
|
55
|
+
/** Cap stamps beyond this multiple of the host-uniform authority flag (§2.9). */
|
|
56
|
+
export const CAP_DIVERGENCE_FACTOR = 4;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The two patterns unambiguous enough for `dev:preflight` (which MAY fail per
|
|
60
|
+
* §2.6) to exit non-zero on: a sustained `off` kill-switch and sustained
|
|
61
|
+
* CI-reason skips on a non-CI host are both graded "like `off`" by the spec —
|
|
62
|
+
* deliberate self-disable signatures. The divergence/watch patterns keep
|
|
63
|
+
* plausible legitimate explanations (emergency env scoping, a deliberate
|
|
64
|
+
* interactive watch) and stay WARN-without-fail even in preflight. The
|
|
65
|
+
* pre-push surface NEVER fails on any of them (structural, §5 last bullet).
|
|
66
|
+
*/
|
|
67
|
+
export const PREFLIGHT_FAIL_PATTERNS = ['sustained-off', 'ci-skip-non-ci-host'];
|
|
68
|
+
|
|
69
|
+
/** Frozen §4 rendezvous paths (mirror of resolveTestRunnerPaths — including
|
|
70
|
+
* the internal INSTAR_HOST_TEST_BASE_DIR test seam). */
|
|
71
|
+
export function resolveLedgerPaths(env = process.env) {
|
|
72
|
+
const override = env['INSTAR_HOST_TEST_BASE_DIR'];
|
|
73
|
+
const baseDir =
|
|
74
|
+
override && override.trim() ? override.trim() : path.join(os.homedir(), '.instar');
|
|
75
|
+
return {
|
|
76
|
+
baseDir,
|
|
77
|
+
ledger: path.join(baseDir, 'host-test-runner-events.jsonl'),
|
|
78
|
+
tuning: path.join(baseDir, 'host-test-runner-tuning.json'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Hardened CI predicate (spec §2.6 — mirrors isCiEnvironment in
|
|
83
|
+
* src/core/testRunnerRunClassifier.ts): CI must be 'true'/'1' AND a positive
|
|
84
|
+
* CI signal must be present. */
|
|
85
|
+
export function isCiHost(env = process.env) {
|
|
86
|
+
const ci = env['CI'];
|
|
87
|
+
if (ci !== 'true' && ci !== '1') return false;
|
|
88
|
+
return env['GITHUB_ACTIONS'] !== undefined || env['RUNNER_OS'] !== undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Rotated ledger segments, newest first (mirror of listLedgerSegments). */
|
|
92
|
+
export function listLedgerSegmentsNewestFirst(paths) {
|
|
93
|
+
try {
|
|
94
|
+
const base = path.basename(paths.ledger).replace(/\.jsonl$/, '');
|
|
95
|
+
return fs
|
|
96
|
+
.readdirSync(paths.baseDir)
|
|
97
|
+
.filter(
|
|
98
|
+
(f) =>
|
|
99
|
+
f.startsWith(`${base}.`) && f.endsWith('.jsonl') && f !== path.basename(paths.ledger),
|
|
100
|
+
)
|
|
101
|
+
.map((f) => path.join(paths.baseDir, f))
|
|
102
|
+
.sort()
|
|
103
|
+
.reverse();
|
|
104
|
+
} catch {
|
|
105
|
+
return []; // no base dir → no segments (nothing to warn about)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Bounded, torn/malformed-line-TOLERANT read of the newest ledger events
|
|
111
|
+
* across the live file + the newest MAX_SEGMENTS_READ rotated segments.
|
|
112
|
+
* Returns `{ events, ledgerPresent }` with events oldest→newest. Never
|
|
113
|
+
* throws; a missing/unreadable file is an empty contribution.
|
|
114
|
+
*/
|
|
115
|
+
export function readLedgerEvents(paths, maxEvents = DEFAULT_MAX_EVENTS) {
|
|
116
|
+
const files = [
|
|
117
|
+
...listLedgerSegmentsNewestFirst(paths).slice(0, MAX_SEGMENTS_READ).reverse(),
|
|
118
|
+
paths.ledger,
|
|
119
|
+
];
|
|
120
|
+
const events = [];
|
|
121
|
+
let ledgerPresent = false;
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
let raw;
|
|
124
|
+
try {
|
|
125
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
126
|
+
ledgerPresent = true;
|
|
127
|
+
} catch {
|
|
128
|
+
continue; // missing live file / segment — empty contribution
|
|
129
|
+
}
|
|
130
|
+
for (const line of raw.split('\n')) {
|
|
131
|
+
if (!line.trim()) continue;
|
|
132
|
+
try {
|
|
133
|
+
const obj = JSON.parse(line);
|
|
134
|
+
if (obj && typeof obj === 'object' && typeof obj.kind === 'string') {
|
|
135
|
+
events.push(obj);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// torn/malformed ledger line — tolerated by contract (§2.6)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { events: events.slice(-maxEvents), ledgerPresent };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sanitizeCap(raw, codeDefault, ceiling) {
|
|
146
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
147
|
+
if (!Number.isInteger(n) || n < 1 || n > ceiling) return codeDefault;
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Tolerant read of the host-uniform tuning authority (spec §2.9). A missing /
|
|
153
|
+
* corrupt file or out-of-range values resolve to code-defaults — this
|
|
154
|
+
* detector must never throw on the same-user-writable file it reads.
|
|
155
|
+
*/
|
|
156
|
+
export function readTuningAuthority(paths) {
|
|
157
|
+
let file = null;
|
|
158
|
+
try {
|
|
159
|
+
const obj = JSON.parse(fs.readFileSync(paths.tuning, 'utf-8'));
|
|
160
|
+
if (obj && typeof obj === 'object') file = obj;
|
|
161
|
+
} catch {
|
|
162
|
+
// absent/corrupt tuning file → code-default authority
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
posture: file?.enforcing === true ? 'enforcing' : 'dry-run',
|
|
166
|
+
suiteCap: sanitizeCap(file?.maxConcurrent, SUITE_CAP_DEFAULT, SUITE_CAP_CEILING),
|
|
167
|
+
targetedCap: sanitizeCap(file?.targetedMax, TARGETED_CAP_DEFAULT, TARGETED_CAP_CEILING),
|
|
168
|
+
ttlSignal: file?.ttlSignal === true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function defaultAuthority() {
|
|
173
|
+
return {
|
|
174
|
+
posture: 'dry-run',
|
|
175
|
+
suiteCap: SUITE_CAP_DEFAULT,
|
|
176
|
+
targetedCap: TARGETED_CAP_DEFAULT,
|
|
177
|
+
ttlSignal: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The §2.6/§2.9 pattern list. Event shapes are the chokepoint's own ledger
|
|
183
|
+
* emissions (tests/setup/test-runner-semaphore.globalSetup.ts):
|
|
184
|
+
* skip → { kind:'skip', reason, loud }
|
|
185
|
+
* warn → { kind:'warn', warnType: 'posture-divergence'|'cap-divergence'|
|
|
186
|
+
* 'env-arm-ignored'|…, … }
|
|
187
|
+
* and every event carries resolved-state stamps (posture, suiteCap,
|
|
188
|
+
* targetedCap, ttlSignalArmed).
|
|
189
|
+
*/
|
|
190
|
+
export const PATTERNS = [
|
|
191
|
+
{
|
|
192
|
+
id: 'sustained-off',
|
|
193
|
+
label: 'sustained INSTAR_HOST_TEST_SEMAPHORE=off skips',
|
|
194
|
+
match: (e) => e.kind === 'skip' && e.reason === 'off',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'ci-skip-non-ci-host',
|
|
198
|
+
label: 'CI-reason skips ledgered on a NON-CI host (spoofed-CI signature; graded like `off`)',
|
|
199
|
+
// Only meaningful when the DETECTING host is not itself a CI runner —
|
|
200
|
+
// gated in detectSelfDisablePatterns().
|
|
201
|
+
requiresNonCiHost: true,
|
|
202
|
+
match: (e) => e.kind === 'skip' && e.reason === 'CI',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'sustained-watch-skip',
|
|
206
|
+
label:
|
|
207
|
+
'sustained defaulted/agent-context watch-mode skips (the labeled-innocent full-suite path)',
|
|
208
|
+
// `loud: true` is stamped exactly for defaulted-into-watch or
|
|
209
|
+
// agent-context watch skips (§2.6); a deliberate human interactive
|
|
210
|
+
// `--watch` is stamped loud:false and never flags.
|
|
211
|
+
match: (e) => e.kind === 'skip' && e.reason === 'watch' && e.loud === true,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'resolved-cap-divergence',
|
|
215
|
+
label: 'resolved cap stamps exceed the host-uniform authority by >4× (env cap-inflation)',
|
|
216
|
+
// Explicit chokepoint warns OR stamp-derived (§2.9: "from the ledger's
|
|
217
|
+
// per-run cap stamps"). The 4× margin makes legitimate authority
|
|
218
|
+
// transitions (caps live in [1, ceiling]) unable to false-positive.
|
|
219
|
+
match: (e, ctx) =>
|
|
220
|
+
(e.kind === 'warn' && e.warnType === 'cap-divergence') ||
|
|
221
|
+
(Number.isFinite(e.suiteCap) &&
|
|
222
|
+
e.suiteCap > ctx.authority.suiteCap * CAP_DIVERGENCE_FACTOR) ||
|
|
223
|
+
(Number.isFinite(e.targetedCap) &&
|
|
224
|
+
e.targetedCap > ctx.authority.targetedCap * CAP_DIVERGENCE_FACTOR),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'posture-divergence',
|
|
228
|
+
label: 'resolved posture diverged from the host-uniform authority (either direction)',
|
|
229
|
+
// Explicit chokepoint-emitted divergence WARNs only (both directions).
|
|
230
|
+
// Deliberately NOT stamp-inferred: comparing historical posture stamps
|
|
231
|
+
// against the CURRENT authority would false-positive across every
|
|
232
|
+
// legitimate posture flip, and the chokepoint already ledgers one
|
|
233
|
+
// explicit warn per divergent run — so a sustained divergence is
|
|
234
|
+
// >= threshold explicit warns by construction.
|
|
235
|
+
match: (e) => e.kind === 'warn' && e.warnType === 'posture-divergence',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'arm-divergence',
|
|
239
|
+
label:
|
|
240
|
+
'env TTL_SIGNAL=1 ignored against an unarmed authority (arming is tuning-file-only)',
|
|
241
|
+
match: (e) => e.kind === 'warn' && e.warnType === 'env-arm-ignored',
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Pure pattern detection over parsed ledger events. Returns
|
|
247
|
+
* `{ findings, eventsScanned }`; a finding is
|
|
248
|
+
* `{ pattern, label, count, threshold, windowMs, lastTs }`.
|
|
249
|
+
* Events with an unparseable `ts` are excluded (never flag on garbage).
|
|
250
|
+
*/
|
|
251
|
+
export function detectSelfDisablePatterns(events, opts = {}) {
|
|
252
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
253
|
+
const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
|
|
254
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
255
|
+
const ciHost = opts.isCiHost ?? isCiHost(opts.env ?? process.env);
|
|
256
|
+
const authority = opts.authority ?? defaultAuthority();
|
|
257
|
+
const ctx = { authority };
|
|
258
|
+
|
|
259
|
+
const recent = (Array.isArray(events) ? events : []).filter((e) => {
|
|
260
|
+
const t = Date.parse(e?.ts);
|
|
261
|
+
return Number.isFinite(t) && nowMs - t <= windowMs;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const findings = [];
|
|
265
|
+
for (const p of PATTERNS) {
|
|
266
|
+
if (p.requiresNonCiHost && ciHost) continue;
|
|
267
|
+
const matched = recent.filter((e) => {
|
|
268
|
+
try {
|
|
269
|
+
return p.match(e, ctx) === true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false; // a hostile/degenerate event never breaks detection
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
if (matched.length >= threshold) {
|
|
275
|
+
findings.push({
|
|
276
|
+
pattern: p.id,
|
|
277
|
+
label: p.label,
|
|
278
|
+
count: matched.length,
|
|
279
|
+
threshold,
|
|
280
|
+
windowMs,
|
|
281
|
+
lastTs: typeof matched[matched.length - 1]?.ts === 'string'
|
|
282
|
+
? matched[matched.length - 1].ts
|
|
283
|
+
: null,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { findings, eventsScanned: recent.length };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Full composition: resolve paths → read ledger (live + newest segments) →
|
|
292
|
+
* read tuning authority → detect. A missing/unreadable ledger returns
|
|
293
|
+
* `{ ledgerPresent: false, findings: [] }` — no ledger, nothing to warn.
|
|
294
|
+
* Never throws.
|
|
295
|
+
*/
|
|
296
|
+
export function runSelfDisableCheck(opts = {}) {
|
|
297
|
+
try {
|
|
298
|
+
const env = opts.env ?? process.env;
|
|
299
|
+
const paths = opts.paths ?? resolveLedgerPaths(env);
|
|
300
|
+
const { events, ledgerPresent } = readLedgerEvents(paths, opts.maxEvents ?? DEFAULT_MAX_EVENTS);
|
|
301
|
+
if (!ledgerPresent) {
|
|
302
|
+
return { ledgerPresent: false, findings: [], eventsScanned: 0, authority: null };
|
|
303
|
+
}
|
|
304
|
+
const authority = opts.authority ?? readTuningAuthority(paths);
|
|
305
|
+
const { findings, eventsScanned } = detectSelfDisablePatterns(events, {
|
|
306
|
+
...opts,
|
|
307
|
+
env,
|
|
308
|
+
authority,
|
|
309
|
+
});
|
|
310
|
+
return { ledgerPresent: true, findings, eventsScanned, authority };
|
|
311
|
+
} catch {
|
|
312
|
+
// Advisory surface: any unexpected internal failure is "nothing to warn".
|
|
313
|
+
return { ledgerPresent: false, findings: [], eventsScanned: 0, authority: null };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Human-readable WARN lines (one per finding) for stderr surfaces. */
|
|
318
|
+
export function formatWarnLines(findings) {
|
|
319
|
+
return (findings ?? []).map(
|
|
320
|
+
(f) =>
|
|
321
|
+
`WARN [test-runner-bound self-disable] ${f.label ?? f.pattern}: ` +
|
|
322
|
+
`${f.count} event(s) within ${Math.round(f.windowMs / 3_600_000)}h ` +
|
|
323
|
+
`(threshold ${f.threshold}${f.lastTs ? `, latest ${f.lastTs}` : ''}) — ` +
|
|
324
|
+
`advisory only; a silently-disabled guard explains more incidents than a broken one (spec §2.6).`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-push-test-runner-selfdisable.mjs — the serverless-host self-disable
|
|
4
|
+
* advisory for the test-runner concurrency bound
|
|
5
|
+
* (docs/specs/test-runner-concurrency-bound.md §2.6(b), §2.9, §5 last bullet).
|
|
6
|
+
*
|
|
7
|
+
* Reads the LOCAL host-test-runner event ledger (live file + newest rotated
|
|
8
|
+
* segments) and WARNs on a sustained self-disable pattern: `off` skips,
|
|
9
|
+
* CI-reason skips on a non-CI host, defaulted/agent-context watch skips,
|
|
10
|
+
* resolved-cap divergence (>4× authority), posture divergence (either
|
|
11
|
+
* direction), and env-arm-ignored events. Detection logic lives ONCE in
|
|
12
|
+
* scripts/lib/test-runner-selfdisable-patterns.mjs (shared with
|
|
13
|
+
* `instar dev:preflight`, which spawns this script with --preflight).
|
|
14
|
+
*
|
|
15
|
+
* STRUCTURAL CONTRACT (spec §2.6 round-4 security + §5 last bullet): in
|
|
16
|
+
* pre-push mode this script exits 0 UNCONDITIONALLY — on findings, on a
|
|
17
|
+
* missing ledger, on a corrupt ledger, and on any internal error. The
|
|
18
|
+
* detection surface for a self-disable must not itself become a new way to
|
|
19
|
+
* wedge the dev loop (§1.1's false-BLOCK inversion): ledger content may only
|
|
20
|
+
* ever ADD a warning, never block a push. A missing/unreadable ledger is
|
|
21
|
+
* silent (no ledger = nothing to warn about).
|
|
22
|
+
* tests/unit/test-runner-selfdisable-patterns.test.ts pins this by spawning
|
|
23
|
+
* this real script against a pattern-heavy fixture ledger and asserting
|
|
24
|
+
* exit 0 with WARN on stderr.
|
|
25
|
+
*
|
|
26
|
+
* --preflight mode (spawned by `instar dev:preflight`, a deliberate human
|
|
27
|
+
* advisory tool that MAY fail per §2.6): exits 1 ONLY for the two unambiguous
|
|
28
|
+
* self-disable signatures (sustained `off`; spoofed CI on a non-CI host —
|
|
29
|
+
* both graded "like `off`" by the spec). The divergence/watch patterns WARN
|
|
30
|
+
* without failing even there (they keep plausible legitimate explanations —
|
|
31
|
+
* emergency env scoping, a deliberate interactive watch).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
PREFLIGHT_FAIL_PATTERNS,
|
|
36
|
+
formatWarnLines,
|
|
37
|
+
runSelfDisableCheck,
|
|
38
|
+
} from './lib/test-runner-selfdisable-patterns.mjs';
|
|
39
|
+
|
|
40
|
+
const preflightMode = process.argv.includes('--preflight');
|
|
41
|
+
let preflightExit = 0;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = runSelfDisableCheck();
|
|
45
|
+
if (result.ledgerPresent && result.findings.length > 0) {
|
|
46
|
+
for (const line of formatWarnLines(result.findings)) {
|
|
47
|
+
process.stderr.write(`⚠️ ${line}\n`);
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
preflightMode &&
|
|
51
|
+
result.findings.some((f) => PREFLIGHT_FAIL_PATTERNS.includes(f.pattern))
|
|
52
|
+
) {
|
|
53
|
+
preflightExit = 1;
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
'preflight: unambiguous self-disable pattern (sustained `off` / spoofed CI) — ' +
|
|
56
|
+
'failing advisorily (§2.6: dev:preflight MAY fail; the pre-push surface never does).\n',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
} else if (preflightMode) {
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
'test-runner bound self-disable ledger check: clean (no sustained pattern, or no local ledger yet).\n',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
// Pre-push mode: a missing/unreadable ledger or zero findings is SILENT.
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// Never let the detection surface wedge the dev loop (§1.1): any internal
|
|
67
|
+
// error is advisory noise at most, in BOTH modes.
|
|
68
|
+
try {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
`test-runner self-disable check errored (advisory, ignored): ${
|
|
71
|
+
err instanceof Error ? err.message : String(err)
|
|
72
|
+
}\n`,
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
/* stderr unavailable — nothing left to do */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// WARN-only: pre-push mode exits 0 unconditionally; only --preflight may
|
|
80
|
+
// carry the advisory failure (and only for PREFLIGHT_FAIL_PATTERNS).
|
|
81
|
+
process.exit(preflightMode ? preflightExit : 0);
|