agentic-workflow-manager 9.21.0 → 9.22.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.
- package/dist/src/commands/change/index.js +16 -5
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/preflight/checks.js +22 -0
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sensors/compatibility/contract.js +56 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
- package/dist/src/commands/sensors/index.js +10 -1
- package/dist/src/commands/sensors/inputs-digest.js +315 -0
- package/dist/src/commands/sensors/prepare.js +10 -0
- package/dist/src/commands/sensors/result-cache.js +278 -0
- package/dist/src/commands/sensors/result.js +31 -12
- package/dist/src/commands/sensors/run.js +265 -8
- package/dist/src/commands/sensors/status.js +66 -7
- package/dist/src/commands/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/receipt.js +219 -9
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +104 -58
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
- package/dist/tests/commands/sensors/result-cache.test.js +352 -0
- package/dist/tests/commands/sensors/run-cache.test.js +434 -0
- package/dist/tests/commands/sensors/run-slice.test.js +171 -0
- package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
- package/dist/tests/commands/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +279 -8
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
|
@@ -33,9 +33,19 @@ function validateRunOptions(opts) {
|
|
|
33
33
|
throw new Error('run option base must be a nonempty string');
|
|
34
34
|
if (opts.readOnly !== undefined && typeof opts.readOnly !== 'boolean')
|
|
35
35
|
throw new Error('run option readOnly must be a boolean');
|
|
36
|
+
if (opts.slice !== undefined && typeof opts.slice !== 'boolean')
|
|
37
|
+
throw new Error('run option slice must be a boolean');
|
|
38
|
+
if (opts.useCache !== undefined && typeof opts.useCache !== 'boolean')
|
|
39
|
+
throw new Error('run option useCache must be a boolean');
|
|
36
40
|
if (opts.changed && opts.ignoreBaseline) {
|
|
37
41
|
throw new Error('refusing to combine --changed with a baseline capture: a partial run cannot define the accepted set');
|
|
38
42
|
}
|
|
43
|
+
if (opts.slice && opts.all) {
|
|
44
|
+
throw new Error('refusing to combine --slice with --all: slice already selects the non-closureOnly set');
|
|
45
|
+
}
|
|
46
|
+
if (opts.slice && opts.slow) {
|
|
47
|
+
throw new Error('refusing to combine --slice with --slow: use a full gate for closureOnly/slow sensors');
|
|
48
|
+
}
|
|
39
49
|
}
|
|
40
50
|
/** Expand the one declared file placeholder into literal structured argv entries. */
|
|
41
51
|
function expandFileInput(command, files) {
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cacheFileName = cacheFileName;
|
|
7
|
+
exports.canCacheRawResult = canCacheRawResult;
|
|
8
|
+
exports.computeSensorFingerprint = computeSensorFingerprint;
|
|
9
|
+
exports.toCacheRaw = toCacheRaw;
|
|
10
|
+
exports.writeResultCache = writeResultCache;
|
|
11
|
+
exports.readResultCache = readResultCache;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const atomic_file_1 = require("../../core/atomic-file");
|
|
16
|
+
const text_safety_1 = require("../../core/changes/text-safety");
|
|
17
|
+
const MAX_CACHE_BYTES = 1024 * 1024;
|
|
18
|
+
const CACHE_SCHEMA = 1;
|
|
19
|
+
function runsDir(root) {
|
|
20
|
+
return path_1.default.join(root, '.awm', 'sensors', 'runs');
|
|
21
|
+
}
|
|
22
|
+
function assertNoSymlinkChain(root) {
|
|
23
|
+
let current = root;
|
|
24
|
+
for (const segment of ['.awm', 'sensors', 'runs']) {
|
|
25
|
+
current = path_1.default.join(current, segment);
|
|
26
|
+
try {
|
|
27
|
+
if (fs_1.default.lstatSync(current).isSymbolicLink())
|
|
28
|
+
throw new Error(`refusing to use symlinked path ${(0, text_safety_1.esc)(current)}`);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
return;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function ensureRunsIgnore(root) {
|
|
38
|
+
const file = path_1.default.join(runsDir(root), '.gitignore');
|
|
39
|
+
try {
|
|
40
|
+
const stat = fs_1.default.lstatSync(file);
|
|
41
|
+
if (stat.isFile() && fs_1.default.readFileSync(file, 'utf8') === '*\n')
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== 'ENOENT')
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
(0, atomic_file_1.writeFileAtomic)(file, '*\n', 0o644);
|
|
49
|
+
}
|
|
50
|
+
function cacheKeyHash(lookup) {
|
|
51
|
+
return crypto_1.default.createHash('sha256')
|
|
52
|
+
.update([lookup.projectKey, lookup.sensorId, lookup.sensorFingerprint, lookup.inputDigest].join('\0'))
|
|
53
|
+
.digest('hex');
|
|
54
|
+
}
|
|
55
|
+
function cacheFileName(lookup) {
|
|
56
|
+
return `cache-${cacheKeyHash(lookup)}.json`;
|
|
57
|
+
}
|
|
58
|
+
/** RF-2.4: store only clean pass, or fail+findings with errors.length > 0 (and not incomplete). */
|
|
59
|
+
function canCacheRawResult(result) {
|
|
60
|
+
if (!result || typeof result !== 'object')
|
|
61
|
+
return false;
|
|
62
|
+
if (result.incomplete)
|
|
63
|
+
return false;
|
|
64
|
+
if (!Array.isArray(result.errors))
|
|
65
|
+
return false;
|
|
66
|
+
if (result.status === 'pass') {
|
|
67
|
+
// Planted pass+errors / pass+failureKind must never cache (or read as a hit).
|
|
68
|
+
return result.errors.length === 0 && result.failureKind === undefined;
|
|
69
|
+
}
|
|
70
|
+
if (result.status !== 'fail')
|
|
71
|
+
return false;
|
|
72
|
+
if (result.failureKind !== 'findings')
|
|
73
|
+
return false;
|
|
74
|
+
return result.errors.length > 0;
|
|
75
|
+
}
|
|
76
|
+
function computeSensorFingerprint(parts, opts) {
|
|
77
|
+
if (!parts || typeof parts !== 'object')
|
|
78
|
+
throw new Error('fingerprint parts must be an object');
|
|
79
|
+
if (typeof parts.cliVersion !== 'string' || parts.cliVersion.length === 0)
|
|
80
|
+
throw new Error('cliVersion must be a nonempty string');
|
|
81
|
+
if (!Array.isArray(parts.inputs) || parts.inputs.some((g) => typeof g !== 'string'))
|
|
82
|
+
throw new Error('inputs must be string[]');
|
|
83
|
+
const allowMissing = opts?.allowMissingLiveVersions === true;
|
|
84
|
+
const toolOk = typeof parts.toolVersion === 'string' && parts.toolVersion.length > 0;
|
|
85
|
+
const runtimeOk = typeof parts.runtimeVersion === 'string' && parts.runtimeVersion.length > 0;
|
|
86
|
+
if (!allowMissing && (!toolOk || !runtimeOk))
|
|
87
|
+
return null;
|
|
88
|
+
const canonical = JSON.stringify({
|
|
89
|
+
variantId: parts.variantId,
|
|
90
|
+
command: parts.command,
|
|
91
|
+
changedCommand: parts.changedCommand ?? null,
|
|
92
|
+
cliVersion: parts.cliVersion,
|
|
93
|
+
inputs: [...parts.inputs].sort(),
|
|
94
|
+
toolVersion: toolOk ? parts.toolVersion : null,
|
|
95
|
+
runtimeVersion: runtimeOk ? parts.runtimeVersion : null,
|
|
96
|
+
});
|
|
97
|
+
return `sha256:${crypto_1.default.createHash('sha256').update(canonical).digest('hex')}`;
|
|
98
|
+
}
|
|
99
|
+
function isSensorError(value) {
|
|
100
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
101
|
+
return false;
|
|
102
|
+
const e = value;
|
|
103
|
+
if (typeof e.message !== 'string' || !(0, text_safety_1.isSingleLine)(e.message))
|
|
104
|
+
return false;
|
|
105
|
+
if (e.file !== undefined && (typeof e.file !== 'string' || !(0, text_safety_1.isSingleLine)(e.file)))
|
|
106
|
+
return false;
|
|
107
|
+
if (e.line !== undefined && (!Number.isSafeInteger(e.line) || e.line < 0))
|
|
108
|
+
return false;
|
|
109
|
+
if (e.column !== undefined && (!Number.isSafeInteger(e.column) || e.column < 0))
|
|
110
|
+
return false;
|
|
111
|
+
if (e.rule !== undefined && typeof e.rule !== 'string')
|
|
112
|
+
return false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function validateRaw(raw) {
|
|
116
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
117
|
+
return 'raw must be an object';
|
|
118
|
+
const r = raw;
|
|
119
|
+
if (typeof r.name !== 'string' || r.name.length === 0)
|
|
120
|
+
return 'raw.name invalid';
|
|
121
|
+
if (r.status !== 'pass' && r.status !== 'fail')
|
|
122
|
+
return 'raw.status invalid';
|
|
123
|
+
if (!Array.isArray(r.errors) || !r.errors.every(isSensorError))
|
|
124
|
+
return 'raw.errors invalid';
|
|
125
|
+
if (r.failureKind !== undefined
|
|
126
|
+
&& r.failureKind !== 'findings'
|
|
127
|
+
&& r.failureKind !== 'missing-binary'
|
|
128
|
+
&& r.failureKind !== 'exit-code')
|
|
129
|
+
return 'raw.failureKind invalid';
|
|
130
|
+
if (r.status === 'pass') {
|
|
131
|
+
if (r.errors.length !== 0)
|
|
132
|
+
return 'raw.pass must have errors.length===0';
|
|
133
|
+
if (r.failureKind !== undefined)
|
|
134
|
+
return 'raw.pass must not carry failureKind';
|
|
135
|
+
}
|
|
136
|
+
if (r.incomplete !== undefined && typeof r.incomplete !== 'string')
|
|
137
|
+
return 'raw.incomplete invalid';
|
|
138
|
+
if (r.skipReason !== undefined && typeof r.skipReason !== 'string')
|
|
139
|
+
return 'raw.skipReason invalid';
|
|
140
|
+
if (r.certification !== undefined
|
|
141
|
+
&& r.certification !== 'certified'
|
|
142
|
+
&& r.certification !== 'operational-unverified'
|
|
143
|
+
&& r.certification !== 'project-declared')
|
|
144
|
+
return 'raw.certification invalid';
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function cacheProblem(x) {
|
|
148
|
+
if (x === null || typeof x !== 'object' || Array.isArray(x))
|
|
149
|
+
return 'cache must be an object';
|
|
150
|
+
const r = x;
|
|
151
|
+
if (r.schema !== CACHE_SCHEMA)
|
|
152
|
+
return 'schema must be 1';
|
|
153
|
+
if (typeof r.sensorId !== 'string' || r.sensorId.length === 0)
|
|
154
|
+
return 'sensorId invalid';
|
|
155
|
+
if (typeof r.projectKey !== 'string' || r.projectKey.length === 0)
|
|
156
|
+
return 'projectKey invalid';
|
|
157
|
+
if (typeof r.sensorFingerprint !== 'string' || !r.sensorFingerprint.startsWith('sha256:'))
|
|
158
|
+
return 'sensorFingerprint invalid';
|
|
159
|
+
if (typeof r.inputDigest !== 'string' || !r.inputDigest.startsWith('sha256:'))
|
|
160
|
+
return 'inputDigest invalid';
|
|
161
|
+
if (r.originCommit !== null && (typeof r.originCommit !== 'string' || !/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(r.originCommit))) {
|
|
162
|
+
return 'originCommit invalid';
|
|
163
|
+
}
|
|
164
|
+
if (typeof r.originDirty !== 'boolean')
|
|
165
|
+
return 'originDirty invalid';
|
|
166
|
+
return validateRaw(r.raw);
|
|
167
|
+
}
|
|
168
|
+
function toCacheRaw(result) {
|
|
169
|
+
if (!result || typeof result !== 'object')
|
|
170
|
+
throw new Error('cache raw requires a sensor result');
|
|
171
|
+
if (result.status !== 'pass' && result.status !== 'fail') {
|
|
172
|
+
throw new Error(`toCacheRaw refuses non-pass/fail status (${result.status}); only pass|fail may be cached`);
|
|
173
|
+
}
|
|
174
|
+
if (!canCacheRawResult(result)) {
|
|
175
|
+
throw new Error('toCacheRaw refuses uncacheable raw (RF-2.4)');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
name: result.name,
|
|
179
|
+
status: result.status,
|
|
180
|
+
errors: result.errors,
|
|
181
|
+
...(result.failureKind ? { failureKind: result.failureKind } : {}),
|
|
182
|
+
...(result.incomplete ? { incomplete: result.incomplete } : {}),
|
|
183
|
+
...(result.certification ? { certification: result.certification } : {}),
|
|
184
|
+
...(result.skipReason ? { skipReason: result.skipReason } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function writeResultCache(gitRoot, record) {
|
|
188
|
+
if (typeof gitRoot !== 'string' || gitRoot.length === 0)
|
|
189
|
+
throw new Error('cache root must be a nonempty string');
|
|
190
|
+
const problem = cacheProblem(record);
|
|
191
|
+
if (problem !== null)
|
|
192
|
+
throw new Error(`invalid result cache: ${problem}`);
|
|
193
|
+
if (record.raw.name !== record.sensorId) {
|
|
194
|
+
throw new Error('refusing to write result cache: raw.name must match sensorId');
|
|
195
|
+
}
|
|
196
|
+
const asResult = {
|
|
197
|
+
name: record.raw.name,
|
|
198
|
+
status: record.raw.status,
|
|
199
|
+
errors: record.raw.errors,
|
|
200
|
+
...(record.raw.failureKind ? { failureKind: record.raw.failureKind } : {}),
|
|
201
|
+
...(record.raw.incomplete ? { incomplete: record.raw.incomplete } : {}),
|
|
202
|
+
...(record.raw.certification ? { certification: record.raw.certification } : {}),
|
|
203
|
+
...(record.raw.skipReason ? { skipReason: record.raw.skipReason } : {}),
|
|
204
|
+
};
|
|
205
|
+
if (!canCacheRawResult(asResult)) {
|
|
206
|
+
throw new Error('refusing to write uncacheable result cache entry (RF-2.4)');
|
|
207
|
+
}
|
|
208
|
+
const lookup = {
|
|
209
|
+
sensorId: record.sensorId,
|
|
210
|
+
projectKey: record.projectKey,
|
|
211
|
+
sensorFingerprint: record.sensorFingerprint,
|
|
212
|
+
inputDigest: record.inputDigest,
|
|
213
|
+
};
|
|
214
|
+
const content = JSON.stringify(record, null, 2) + '\n';
|
|
215
|
+
if (Buffer.byteLength(content) > MAX_CACHE_BYTES)
|
|
216
|
+
throw new Error('invalid result cache: larger than the size read accepts');
|
|
217
|
+
assertNoSymlinkChain(gitRoot);
|
|
218
|
+
fs_1.default.mkdirSync(runsDir(gitRoot), { recursive: true, mode: 0o700 });
|
|
219
|
+
assertNoSymlinkChain(gitRoot);
|
|
220
|
+
ensureRunsIgnore(gitRoot);
|
|
221
|
+
const file = path_1.default.join(runsDir(gitRoot), cacheFileName(lookup));
|
|
222
|
+
(0, atomic_file_1.writeFileAtomic)(file, content, 0o600);
|
|
223
|
+
fs_1.default.chmodSync(file, 0o600);
|
|
224
|
+
return file;
|
|
225
|
+
}
|
|
226
|
+
/** Validated cache read (RF-2.15). Any invalid/tampered entry is a miss. */
|
|
227
|
+
function readResultCache(gitRoot, lookup) {
|
|
228
|
+
if (typeof gitRoot !== 'string' || gitRoot.length === 0)
|
|
229
|
+
return null;
|
|
230
|
+
if (!lookup || typeof lookup !== 'object')
|
|
231
|
+
return null;
|
|
232
|
+
if (typeof lookup.sensorId !== 'string' || typeof lookup.projectKey !== 'string')
|
|
233
|
+
return null;
|
|
234
|
+
if (typeof lookup.sensorFingerprint !== 'string' || typeof lookup.inputDigest !== 'string')
|
|
235
|
+
return null;
|
|
236
|
+
let raw;
|
|
237
|
+
try {
|
|
238
|
+
assertNoSymlinkChain(gitRoot);
|
|
239
|
+
const file = path_1.default.join(runsDir(gitRoot), cacheFileName(lookup));
|
|
240
|
+
const stat = fs_1.default.lstatSync(file);
|
|
241
|
+
if (!stat.isFile() || stat.size > MAX_CACHE_BYTES)
|
|
242
|
+
return null;
|
|
243
|
+
raw = fs_1.default.readFileSync(file, 'utf8');
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
let parsed;
|
|
249
|
+
try {
|
|
250
|
+
parsed = JSON.parse(raw);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
if (cacheProblem(parsed) !== null)
|
|
256
|
+
return null;
|
|
257
|
+
const record = parsed;
|
|
258
|
+
if (record.sensorId !== lookup.sensorId
|
|
259
|
+
|| record.projectKey !== lookup.projectKey
|
|
260
|
+
|| record.sensorFingerprint !== lookup.sensorFingerprint
|
|
261
|
+
|| record.inputDigest !== lookup.inputDigest)
|
|
262
|
+
return null;
|
|
263
|
+
if (record.raw.name !== record.sensorId || record.raw.name !== lookup.sensorId)
|
|
264
|
+
return null;
|
|
265
|
+
// RF-2.15/2.4: structural validity is not enough — reject uncacheable raw payloads.
|
|
266
|
+
const asResult = {
|
|
267
|
+
name: record.raw.name,
|
|
268
|
+
status: record.raw.status,
|
|
269
|
+
errors: record.raw.errors,
|
|
270
|
+
...(record.raw.failureKind ? { failureKind: record.raw.failureKind } : {}),
|
|
271
|
+
...(record.raw.incomplete ? { incomplete: record.raw.incomplete } : {}),
|
|
272
|
+
...(record.raw.certification ? { certification: record.raw.certification } : {}),
|
|
273
|
+
...(record.raw.skipReason ? { skipReason: record.raw.skipReason } : {}),
|
|
274
|
+
};
|
|
275
|
+
if (!canCacheRawResult(asResult))
|
|
276
|
+
return null;
|
|
277
|
+
return record;
|
|
278
|
+
}
|
|
@@ -77,6 +77,9 @@ function scoped(result, prepared) {
|
|
|
77
77
|
...(prepared.effectiveScope === 'changed' ? { scope: 'changed' } : {}),
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
function withFailureKind(result, kind) {
|
|
81
|
+
return result.status === 'fail' ? { ...result, failureKind: kind } : result;
|
|
82
|
+
}
|
|
80
83
|
/** Interpret one raw bounded process result without knowing its manifest format. */
|
|
81
84
|
function interpretResult(prepared, raw) {
|
|
82
85
|
validatePrepared(prepared);
|
|
@@ -90,13 +93,15 @@ function interpretResult(prepared, raw) {
|
|
|
90
93
|
execution,
|
|
91
94
|
});
|
|
92
95
|
const format = formatterFor(prepared.name, prepared.formatter);
|
|
93
|
-
if (raw.spawnError)
|
|
94
|
-
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor could not be started: ${raw.spawnError.message}` }] });
|
|
96
|
+
if (raw.spawnError) {
|
|
97
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: `sensor could not be started: ${raw.spawnError.message}` }] }, 'missing-binary'));
|
|
98
|
+
}
|
|
95
99
|
if (raw.timedOut || raw.overflowed) {
|
|
96
100
|
const reason = raw.timedOut ? `timeout after ${prepared.timeoutMs}ms` : `output exceeded ${MAX_BUFFER} bytes`;
|
|
97
101
|
const errors = format(raw.stdout + raw.stderr);
|
|
98
|
-
if (errors.length > 0)
|
|
99
|
-
return withEvidence({ name: prepared.name, status: 'fail', errors, incomplete: `${reason} — findings below are from partial output; the run did not finish` });
|
|
102
|
+
if (errors.length > 0) {
|
|
103
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors, incomplete: `${reason} — findings below are from partial output; the run did not finish` }, 'findings'));
|
|
104
|
+
}
|
|
100
105
|
return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: reason });
|
|
101
106
|
}
|
|
102
107
|
if (raw.code === 0) {
|
|
@@ -104,27 +109,31 @@ function interpretResult(prepared, raw) {
|
|
|
104
109
|
return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
|
|
105
110
|
}
|
|
106
111
|
const errors = format(raw.stdout);
|
|
107
|
-
|
|
112
|
+
if (errors.length === 0)
|
|
113
|
+
return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
|
|
114
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors }, 'findings'));
|
|
108
115
|
}
|
|
109
116
|
const output = raw.stdout + raw.stderr;
|
|
110
117
|
if (prepared.formatter === 'exit-code' || format === parseExitCodeOutput) {
|
|
111
118
|
const combined = output.trim();
|
|
112
119
|
const evidence = truncateEvidence(combined.length > 0 ? combined : `exit ${raw.code}`);
|
|
113
|
-
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: evidence }] });
|
|
120
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: evidence }] }, 'exit-code'));
|
|
114
121
|
}
|
|
115
122
|
const errors = format(output);
|
|
116
123
|
if (errors.length > 0)
|
|
117
|
-
return withEvidence({ name: prepared.name, status: 'fail', errors });
|
|
124
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors }, 'findings'));
|
|
118
125
|
const lower = output.toLowerCase();
|
|
119
126
|
const toolMissing = raw.code === 127
|
|
120
127
|
|| lower.includes('command not found')
|
|
121
128
|
|| lower.includes('is not recognized as an internal or external command')
|
|
122
129
|
|| lower.includes('enoent')
|
|
123
130
|
|| lower.includes('could not determine executable');
|
|
124
|
-
if (toolMissing)
|
|
125
|
-
return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${truncateEvidence(output)}` }] });
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
if (toolMissing) {
|
|
132
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${truncateEvidence(output)}` }] }, 'missing-binary'));
|
|
133
|
+
}
|
|
134
|
+
if (prepared.name === 'test') {
|
|
135
|
+
return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: `SENSOR[${prepared.name}] failed (exit ${raw.code})` }] }, 'exit-code'));
|
|
136
|
+
}
|
|
128
137
|
return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: `exit ${raw.code}: ${truncateEvidence(output)}` });
|
|
129
138
|
}
|
|
130
139
|
/** Execute a validated prepared command, or render its deliberate synthetic result. */
|
|
@@ -154,5 +163,15 @@ function applyBaseline(result, accepted) {
|
|
|
154
163
|
const { newErrors, suppressed } = (0, baseline_1.partition)(result.name, result.errors, accepted);
|
|
155
164
|
if (suppressed === 0)
|
|
156
165
|
return result;
|
|
157
|
-
|
|
166
|
+
const status = newErrors.length > 0 ? 'fail' : 'pass';
|
|
167
|
+
const { failureKind: _drop, ...rest } = result;
|
|
168
|
+
return {
|
|
169
|
+
...rest,
|
|
170
|
+
errors: newErrors,
|
|
171
|
+
status,
|
|
172
|
+
// failureKind only applies to fail; a baseline-suppressed pass must not keep it (QA-4).
|
|
173
|
+
...(status === 'fail' && result.failureKind ? { failureKind: result.failureKind } : {}),
|
|
174
|
+
newCount: newErrors.length,
|
|
175
|
+
baselineCount: suppressed,
|
|
176
|
+
};
|
|
158
177
|
}
|