amsd-pipeline-shared 0.5.7 → 0.5.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.
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"node": {
|
|
4
4
|
"runtimeNames": ["TypeError", "ReferenceError", "SyntaxError", "RangeError"],
|
|
5
5
|
"runtimeMessage": "Unexpected token|is not valid JSON|Unexpected end of JSON|Cannot read propert|Cannot set propert|is not a function|is not defined|is not iterable|Cannot destructure|is not a constructor|Invalid array length|Maximum call stack",
|
|
6
|
-
"external": "\\bE(NOSPC|ACCES|PERM|MFILE|NOMEM|CONNRESET|CONNREFUSED|TIMEDOUT|NOTFOUND|AI_AGAIN|PIPE)\\b|bounded:
|
|
6
|
+
"external": "\\bE(NOSPC|ACCES|PERM|MFILE|NOMEM|CONNRESET|CONNREFUSED|TIMEDOUT|NOTFOUND|AI_AGAIN|PIPE)\\b|bounded: REFUSED|exited (137|143)\\b|\\b(401|403|429|500|502|503|529)\\b|overloaded|rate.?limit|credit|quota|<!DOCTYPE|<html|out of memory|killed",
|
|
7
|
+
"ownBudget": "bounded: cap=\\d+MB",
|
|
7
8
|
"generalRefusal": "refused|usage:",
|
|
8
9
|
"internalFrame": "^node:",
|
|
9
10
|
"dependencyDir": "node_modules"
|
|
@@ -19,12 +19,16 @@ function signaturesFor(runtime, injected) {
|
|
|
19
19
|
const all = injected || JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'config', 'doctor-signatures.json'), 'utf8'));
|
|
20
20
|
const s = all[runtime];
|
|
21
21
|
if (!s) throw new Error(`no signatures for runtime "${runtime}" (config/doctor-signatures.json knows: ${Object.keys(all).filter((k) => !k.startsWith('$')).join(', ')})`);
|
|
22
|
-
return { runtimeNames: new Set(s.runtimeNames), runtimeMessage: rxOf(s.runtimeMessage), external: rxOf(s.external), generalRefusal: rxOf(s.generalRefusal),
|
|
22
|
+
return { runtimeNames: new Set(s.runtimeNames), runtimeMessage: rxOf(s.runtimeMessage), external: rxOf(s.external), ownBudget: s.ownBudget ? rxOf(s.ownBudget) : /$^/, generalRefusal: rxOf(s.generalRefusal),
|
|
23
23
|
internalFrame: rxOf(s.internalFrame), dependencyDir: s.dependencyDir };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function classify({ profile, portRoot, res, crash, aborted, since, signatures, evidence }) {
|
|
27
27
|
const sig = signaturesFor(profile.classify.runtime, signatures);
|
|
28
|
+
const takesMost = profile.classify.policy === 'all-but-refusals';
|
|
29
|
+
// WHAT IS EXTERNAL: the machine, the network, the model service, a kill. Under "all-but-refusals" a kill that happened inside the pipeline's OWN memory budget (the bounded wrapper's start banner is in the text:
|
|
30
|
+
// the cap the pipeline chose, so what ran inside it is the pipeline's to bound — run 11, jest workers inside a Writer session's cap) is not external
|
|
31
|
+
const isExternal = (text) => sig.external.test(text) && !(takesMost && sig.ownBudget.test(text));
|
|
28
32
|
if (!res || typeof res !== 'object') return no('no-result', 'the leg gave no result');
|
|
29
33
|
if (aborted) return no('aborted', 'the operator or a watcher ended the leg');
|
|
30
34
|
// AN AGENT-CAUSED ABORT (operator, 2026-10-20: look for the agentic solution first): a failed leg whose reason is one of the pipeline's own agent-caused
|
|
@@ -39,7 +43,7 @@ function classify({ profile, portRoot, res, crash, aborted, since, signatures, e
|
|
|
39
43
|
// has a number and its reason and matches none. The marked line is still refused if it names the machine, the network, the model service or a kill.
|
|
40
44
|
const line = evidence && profile.classify.faultMarks.length ? String(evidence).split('\n').find((l) => profile.classify.faultMarks.some((r) => r.test(l))) : null;
|
|
41
45
|
if (line) {
|
|
42
|
-
if (
|
|
46
|
+
if (isExternal(line)) return no('external', `the failing line also names the machine, the network, the model service or a kill — not the pipeline: ${line.slice(0, 160)}`);
|
|
43
47
|
return { call: true, kind: 'fault', layer: 'undecided', why: `the leg failed without a verdict: ${line.trim().slice(0, 200)}` };
|
|
44
48
|
}
|
|
45
49
|
// MOST FAILURES (profile classify.policy "all-but-refusals"): the pipeline's own refusals are its decision and what names the machine, the network, the model service or a kill is not the code —
|
|
@@ -48,21 +52,21 @@ function classify({ profile, portRoot, res, crash, aborted, since, signatures, e
|
|
|
48
52
|
const lines = String(evidence || '').split('\n').filter((l) => l.trim());
|
|
49
53
|
const window = [reason, ...lines.slice(-25)].filter(Boolean).join('\n');
|
|
50
54
|
if (!window.trim()) return no('not-a-crash', 'the leg failed with no reason and no evidence — nothing to investigate');
|
|
51
|
-
if (
|
|
55
|
+
if (isExternal(window)) return no('external', 'the end of the failure also names the machine, the network, the model service or a kill — not the pipeline');
|
|
52
56
|
if (profile.classify.refusals.some((r) => r.test(window))) return no('refusal', 'the pipeline stopped on purpose (its own refusal or pre-condition)');
|
|
53
57
|
const last = [...lines].reverse().find((l) => /fail|HALT|block|error/i.test(l)) || reason;
|
|
54
58
|
return { call: true, kind: 'failure', layer: 'undecided', why: `the leg failed (${reason || 'no reason'}): ${String(last).trim().slice(0, 200)}` };
|
|
55
59
|
}
|
|
56
60
|
return no('not-a-crash', 'the leg failed on a verdict that names no agent-caused message — reported, not repaired');
|
|
57
61
|
}
|
|
58
|
-
if (
|
|
62
|
+
if (isExternal(reason)) return no('external', 'the agent-caused message also names the machine, the network, the model service or a kill — not the pipeline');
|
|
59
63
|
return { call: true, kind: 'agentic-abort', layer: 'agentic', why: `an agent caused the abort: ${reason.slice(0, 200)}` };
|
|
60
64
|
}
|
|
61
65
|
if (res.status !== 'crashed') return no(res.status === 'no-answer' ? 'no-record' : 'not-a-crash', `the leg ended ${res.status}, not crashed — a kill, a verdict or a pause is reported, not repaired`);
|
|
62
66
|
if (!crash || typeof crash !== 'object') return no('no-record', 'no crash record: nothing proves a defect in the code');
|
|
63
67
|
if (!crash.at || !since || Date.parse(crash.at) < Date.parse(since)) return no('stale-record', 'the crash record is older than this leg');
|
|
64
68
|
const text = `${crash.name || ''}: ${crash.message || ''}`;
|
|
65
|
-
if (
|
|
69
|
+
if (isExternal(text)) return no('external', 'the failure names the machine, the network, the model service or a kill — not the code');
|
|
66
70
|
if (sig.generalRefusal.test(text) || profile.classify.refusals.some((r) => r.test(text))) return no('refusal', 'the pipeline stopped on purpose');
|
|
67
71
|
const site = (crash.frames || []).find((f) => f && typeof f.file === 'string' && !sig.internalFrame.test(f.file)) || null;
|
|
68
72
|
if (!site) return no('outside-port', 'no frame of the stack is in any file');
|
|
@@ -71,6 +75,8 @@ function classify({ profile, portRoot, res, crash, aborted, since, signatures, e
|
|
|
71
75
|
const inPort = !!portRoot && !rel.startsWith('..') && !path.isAbsolute(rel) && profile.scope.repairDirs.includes(parts[0]) && !parts.includes(sig.dependencyDir);
|
|
72
76
|
if (!inPort) return no('outside-port', `the throw site ${site.file}:${site.line} is not the component's own code`);
|
|
73
77
|
if (crash.fault === true) return { call: true, kind: 'fault', layer: 'undecided', why: `a fault the component raised: ${String(crash.message).slice(0, 160)} — at ${rel}:${site.line}${crash.step ? ` (step: ${crash.step})` : ''}` };
|
|
78
|
+
// under "all-but-refusals" an Error thrown by the component's own code is the doctor's even without a runtime-exception mark (run 11: `the live session for writer exited null`)
|
|
79
|
+
if (takesMost && !sig.runtimeNames.has(crash.name) && !sig.runtimeMessage.test(crash.message || '')) return { call: true, kind: 'crash', layer: 'undecided', why: `${crash.name}: ${String(crash.message).slice(0, 160)} — thrown at ${rel}:${site.line}${crash.step ? ` (step: ${crash.step})` : ''}` };
|
|
74
80
|
if (!sig.runtimeNames.has(crash.name) && !sig.runtimeMessage.test(crash.message || '')) return no('ambiguous', `an Error in ${rel}:${site.line} with no sign of a code defect — reported`);
|
|
75
81
|
return { call: true, kind: 'port-defect', layer: 'code', why: `${crash.name}: ${String(crash.message).slice(0, 160)} — thrown at ${rel}:${site.line}${crash.step ? ` (step: ${crash.step})` : ''}` };
|
|
76
82
|
}
|
|
@@ -58,6 +58,9 @@ function validate(input) {
|
|
|
58
58
|
if (need('test', t, 'envUnset')) out.test.envUnset = strs('test.envUnset', t.envUnset, { nonEmpty: false });
|
|
59
59
|
if (need('test', t, 'greenPatterns')) { if (Array.isArray(t.greenPatterns) && !t.greenPatterns.length) bad('test.greenPatterns', 'at least one pattern is required'); out.test.greenPatterns = rxs('test.greenPatterns', t.greenPatterns); }
|
|
60
60
|
if (need('test', t, 'failedPattern')) out.test.failedPattern = rx('test.failedPattern', t.failedPattern);
|
|
61
|
+
// OPTIONAL: repository paths that git ignores and the tests read (a codeline copy, generated data) — linked into every throwaway worktree the verification runs its tests in
|
|
62
|
+
out.test.fixtures = has(t, 'fixtures') ? (strs('test.fixtures', t.fixtures, { nonEmpty: false }) || []) : [];
|
|
63
|
+
out.test.fixtures.forEach((d, i) => rel(`test.fixtures[${i}]`, d));
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
const i = p.install;
|
|
@@ -34,12 +34,23 @@ function runTests(wt, files, profile) {
|
|
|
34
34
|
return { status: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A throwaway work tree of <rev>; removed by the caller's finally. `fixtures` (the profile's test.fixtures): repository paths git ignores and the tests read — each is linked in from the repository, so a
|
|
39
|
+
* correct repair is judged on the data its tests need (run 11: the doctor's own GREEN check failed on a missing git-ignored codeline copy). A fixture the repository does not have is a stated error.
|
|
40
|
+
*/
|
|
41
|
+
function worktree(repo, rev, fixtures = []) {
|
|
39
42
|
const wt = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-wt-'));
|
|
40
43
|
fs.rmSync(wt, { recursive: true, force: true });
|
|
41
44
|
const r = git(repo, 'worktree', 'add', '--detach', '-q', wt, rev);
|
|
42
45
|
if (r.status !== 0) throw new Error(`worktree of ${rev}: ${r.stderr}`);
|
|
46
|
+
try {
|
|
47
|
+
for (const f of fixtures) {
|
|
48
|
+
const from = path.join(repo, f); const to = path.join(wt, f);
|
|
49
|
+
if (!fs.existsSync(from)) throw new Error(`test.fixtures names ${f}, which is not in ${repo} — nothing to link into the worktree`);
|
|
50
|
+
if (fs.existsSync(to)) continue;
|
|
51
|
+
fs.mkdirSync(path.dirname(to), { recursive: true }); fs.symlinkSync(from, to);
|
|
52
|
+
}
|
|
53
|
+
} catch (e) { drop(repo, wt); throw e; }
|
|
43
54
|
return wt;
|
|
44
55
|
}
|
|
45
56
|
const drop = (repo, wt) => { git(repo, 'worktree', 'remove', '--force', wt); git(repo, 'worktree', 'prune'); fs.rmSync(wt, { recursive: true, force: true }); };
|
|
@@ -72,13 +83,13 @@ function verifyRepair({ repo, base, branch, profile, layer, abortKind }) {
|
|
|
72
83
|
let wtBase = null; let wtTip = null;
|
|
73
84
|
try {
|
|
74
85
|
// RED: the base, with ONLY the repair's tests added — they must fail there (a test that cannot even load is caught by the GREEN run)
|
|
75
|
-
wtBase = worktree(repo, base);
|
|
86
|
+
wtBase = worktree(repo, base, profile.test.fixtures);
|
|
76
87
|
const co = git(wtBase, 'checkout', branch, '--', ...tests);
|
|
77
88
|
if (co.status !== 0) return bad(`cannot place the tests on the base: ${co.stderr.trim()}`, { files });
|
|
78
89
|
const red = runTests(wtBase, tests, profile);
|
|
79
90
|
if (red.status === 0) return bad('the test passes without the fix: it is not RED, so it proves nothing', { files, red: red.out });
|
|
80
91
|
// GREEN: the branch's tip
|
|
81
|
-
wtTip = worktree(repo, branch);
|
|
92
|
+
wtTip = worktree(repo, branch, profile.test.fixtures);
|
|
82
93
|
const green = runTests(wtTip, tests, profile);
|
|
83
94
|
if (green.status !== 0 || !profile.test.greenPatterns.every((r) => r.test(green.out))) return bad('the test fails with the fix: not GREEN', { files, red: red.out, green: green.out });
|
|
84
95
|
return { ok: true, why: 'RED on the base, GREEN with the fix', files, tests, red: red.out, green: green.out };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amsd-pipeline-shared",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "The AMSD pipeline's shared components, one source for the bash pipeline and its Claude workflows: the model-call gateway (llm-handler.sh — retry, ladder, self-heal, cost) and the libraries it runs.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|