agentic-workflow-manager 6.5.2 → 8.0.0
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/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +261 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +59 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +126 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +96 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +150 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +92 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +98 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
- package/dist/tests/commands/sensors/exec.test.js +67 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +132 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -9,6 +9,41 @@ exports.buildManifest = buildManifest;
|
|
|
9
9
|
exports.initSensors = initSensors;
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const contract_1 = require("./compatibility/contract");
|
|
13
|
+
const materialize_1 = require("./compatibility/materialize");
|
|
14
|
+
const live_1 = require("./compatibility/live");
|
|
15
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
16
|
+
const pack_source_1 = require("./compatibility/pack-source");
|
|
17
|
+
/** Read a pack only through the shared source resolver. Init is a registry
|
|
18
|
+
* boundary: a pack path that is absent is harmless, but a symlink or escaping
|
|
19
|
+
* claimed source is never silently downgraded to an empty manifest. */
|
|
20
|
+
function readResolvedPack(pack, registryRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const source = (0, pack_source_1.resolvePackSource)(pack, {
|
|
23
|
+
registries: [{ name: 'init-registry', remote: 'local', contentRoot: registryRoot }],
|
|
24
|
+
});
|
|
25
|
+
return { path: source.path, content: source.content };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && error.message.includes('was not found in configured registries'))
|
|
29
|
+
return null;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readV2Pack(pack, registryRoot) {
|
|
34
|
+
const source = readResolvedPack(pack, registryRoot);
|
|
35
|
+
if (!source)
|
|
36
|
+
return null;
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = JSON.parse(source.content);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error(`cannot parse sensor pack ${source.path}`);
|
|
43
|
+
}
|
|
44
|
+
const parsed = (0, contract_1.parseSensorPack)(raw, source.path);
|
|
45
|
+
return parsed.kind === 'v2' ? { pack: parsed.pack, packRoot: path_1.default.dirname(source.path) } : null;
|
|
46
|
+
}
|
|
12
47
|
const STACK_DETECTORS = [
|
|
13
48
|
{ pack: 'js-ts', files: ['package.json'] },
|
|
14
49
|
{ pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
|
|
@@ -60,12 +95,12 @@ function detectSourceDirs(cwd) {
|
|
|
60
95
|
* with the project's actual source dirs. Returns null if the pack has no pack.json.
|
|
61
96
|
*/
|
|
62
97
|
function readPackDefaults(pack, registryRoot, cwd) {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
98
|
+
const source = readResolvedPack(pack, registryRoot);
|
|
99
|
+
if (!source)
|
|
65
100
|
return null;
|
|
66
101
|
let parsed;
|
|
67
102
|
try {
|
|
68
|
-
parsed = JSON.parse(
|
|
103
|
+
parsed = JSON.parse(source.content);
|
|
69
104
|
}
|
|
70
105
|
catch {
|
|
71
106
|
return null;
|
|
@@ -174,7 +209,7 @@ function assertPackExists(pack, registryRoot) {
|
|
|
174
209
|
throw new Error(`pack '${pack}' not found in registry (available: ${available.join(', ')})`);
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
|
-
function initSensors(opts = {}) {
|
|
212
|
+
async function initSensors(opts = {}) {
|
|
178
213
|
const cwd = opts.cwd ?? process.cwd();
|
|
179
214
|
const configure = opts.configure ?? true; // configure (copy pack config files) by default
|
|
180
215
|
const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
|
|
@@ -192,15 +227,66 @@ function initSensors(opts = {}) {
|
|
|
192
227
|
detection = detectStack(cwd);
|
|
193
228
|
}
|
|
194
229
|
let existing;
|
|
230
|
+
let existingV2;
|
|
195
231
|
if (fs_1.default.existsSync(manifestPath)) {
|
|
232
|
+
let raw;
|
|
196
233
|
try {
|
|
197
|
-
|
|
234
|
+
raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
198
235
|
}
|
|
199
|
-
catch {
|
|
236
|
+
catch {
|
|
237
|
+
throw new Error(`cannot parse existing sensor manifest ${manifestPath}`);
|
|
238
|
+
}
|
|
239
|
+
// Never overwrite a corrupt or future-version manifest during init. Legacy
|
|
240
|
+
// manifests remain a supported migration input; v2 is validated before a new
|
|
241
|
+
// materialized selection becomes the commit point.
|
|
242
|
+
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
243
|
+
if (parsed.kind === 'legacy')
|
|
244
|
+
existing = parsed.pack;
|
|
245
|
+
else
|
|
246
|
+
existingV2 = parsed.pack;
|
|
200
247
|
}
|
|
201
248
|
// `detection` keeps saying what the tree IS; `pack` is what the registry can serve
|
|
202
249
|
// for it. They diverge only when the registry lacks the detected pack.
|
|
203
250
|
const { pack: resolvedPack, unavailablePack } = resolvePack(detection.pack, opts.registryRoot);
|
|
251
|
+
// v2 packs own the executable contract. Legacy packs deliberately retain the
|
|
252
|
+
// old string-command path below until a registry author publishes a v2 contract.
|
|
253
|
+
if (opts.registryRoot) {
|
|
254
|
+
const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
|
|
255
|
+
if (resolvedV2) {
|
|
256
|
+
const compatibility = (await (0, live_1.resolveParsedPackCompatibility)(cwd, resolvedV2.pack)).sensors;
|
|
257
|
+
const sensors = {};
|
|
258
|
+
for (const [name, sensor] of Object.entries(resolvedV2.pack.sensors)) {
|
|
259
|
+
const resolved = compatibility[name];
|
|
260
|
+
const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
|
|
261
|
+
// Unresolved states do not receive an arbitrary command. They remain
|
|
262
|
+
// represented in the manifest so status/coverage can explain them,
|
|
263
|
+
// but run cannot accidentally dispatch a mismatched executable.
|
|
264
|
+
if (variant) {
|
|
265
|
+
const prior = existing?.sensors[name] ?? existingV2?.sensors[name];
|
|
266
|
+
// Legacy string commands cannot be translated into a shell-free
|
|
267
|
+
// structured command without guessing. Preserve the operator's
|
|
268
|
+
// enabled/fast intent but make that migration non-certified.
|
|
269
|
+
const migratedOverride = existing?.sensors[name]?.cmd !== undefined;
|
|
270
|
+
sensors[name] = {
|
|
271
|
+
enabled: prior?.enabled ?? true,
|
|
272
|
+
fast: prior?.fast ?? sensor.fast ?? false,
|
|
273
|
+
variantId: variant.id,
|
|
274
|
+
command: variant.command,
|
|
275
|
+
assets: variant.assets,
|
|
276
|
+
initializedCompatibility: migratedOverride
|
|
277
|
+
? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
|
|
278
|
+
: resolved,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const materialized = (0, materialize_1.materializeResolvedSensors)({
|
|
283
|
+
projectRoot: cwd, packRoot: resolvedV2.packRoot,
|
|
284
|
+
pack: resolvedPack, registryRoot: opts.registryRoot, sensors, configure,
|
|
285
|
+
});
|
|
286
|
+
return { detection, ...materialized,
|
|
287
|
+
compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
204
290
|
const manifest = buildManifest(resolvedPack, existing, opts.registryRoot, cwd);
|
|
205
291
|
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
206
292
|
const tmpPath = manifestPath + '.tmp';
|
|
@@ -26,6 +26,8 @@ const changed_1 = require("./changed");
|
|
|
26
26
|
// verbo de lectura: no debe tener a mano ninguna funcion capaz de mutar el proyecto.
|
|
27
27
|
const init_1 = require("./init");
|
|
28
28
|
const paths_1 = require("../../core/paths");
|
|
29
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
30
|
+
const live_1 = require("./compatibility/live");
|
|
29
31
|
const MANIFEST_FILE = '.awm/sensors.json';
|
|
30
32
|
const DEFAULT_FAST_TIMEOUT = 10_000;
|
|
31
33
|
const DEFAULT_SLOW_TIMEOUT = 120_000;
|
|
@@ -67,6 +69,28 @@ function readManifest(cwd) {
|
|
|
67
69
|
return null;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
async function resolveLiveV2(cwd, manifest) {
|
|
73
|
+
if (manifest.kind !== 'v2')
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
return await (0, live_1.resolveLiveCompatibility)(cwd, manifest.pack.pack, manifest.pack.registryRoot);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function runV2Sensor(name, command, timeout, cwd, formatter) {
|
|
83
|
+
const res = await (0, exec_1.runStructuredCommand)(command, { timeout, cwd, maxBuffer: MAX_BUFFER });
|
|
84
|
+
const format = getFormatter(name, formatter);
|
|
85
|
+
if (res.spawnError)
|
|
86
|
+
return { name, status: 'fail', errors: [{ message: `sensor could not be started: ${res.spawnError.message}` }] };
|
|
87
|
+
if (res.timedOut || res.overflowed)
|
|
88
|
+
return { name, status: 'inconclusive', errors: [], skipReason: res.timedOut ? `timeout after ${timeout}ms` : `output exceeded ${MAX_BUFFER} bytes` };
|
|
89
|
+
const errors = format(res.stdout + res.stderr);
|
|
90
|
+
if (res.code === 0)
|
|
91
|
+
return { name, status: errors.length ? 'fail' : 'pass', errors };
|
|
92
|
+
return errors.length ? { name, status: 'fail', errors } : { name, status: 'inconclusive', errors: [], skipReason: `exit ${res.code}` };
|
|
93
|
+
}
|
|
70
94
|
/**
|
|
71
95
|
* Detect — and only detect — that the manifest's pack no longer describes the tree:
|
|
72
96
|
* it sits on the `generic` fallback while real stack indicators (package.json,
|
|
@@ -105,6 +129,10 @@ function findManifestDir(startCwd) {
|
|
|
105
129
|
while (true) {
|
|
106
130
|
if (fs_1.default.existsSync(path_1.default.join(dir, MANIFEST_FILE)))
|
|
107
131
|
return dir;
|
|
132
|
+
// Temp directories are test/scratch boundaries, never project ancestors.
|
|
133
|
+
// This prevents an unrelated /tmp/.awm from being adopted by a fresh project.
|
|
134
|
+
if (dir === path_1.default.resolve(os_1.default.tmpdir()))
|
|
135
|
+
return null;
|
|
108
136
|
const parent = path_1.default.dirname(dir);
|
|
109
137
|
if (parent === dir)
|
|
110
138
|
return null; // reached filesystem root
|
|
@@ -282,6 +310,45 @@ async function runSensors(opts = {}) {
|
|
|
282
310
|
const manifest = readManifest(manifestDir);
|
|
283
311
|
if (!manifest)
|
|
284
312
|
return { sensors: [], overall: 'not_certified' };
|
|
313
|
+
let parsedManifest;
|
|
314
|
+
try {
|
|
315
|
+
parsedManifest = (0, manifest_1.parseSensorManifest)(JSON.parse(fs_1.default.readFileSync(path_1.default.join(manifestDir, MANIFEST_FILE), 'utf8')), path_1.default.join(manifestDir, MANIFEST_FILE));
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return { sensors: [], overall: 'not_certified' };
|
|
319
|
+
}
|
|
320
|
+
if (parsedManifest.kind === 'v2') {
|
|
321
|
+
const live = await resolveLiveV2(manifestDir, parsedManifest);
|
|
322
|
+
const tasks = [];
|
|
323
|
+
for (const [name, sensor] of Object.entries(parsedManifest.pack.sensors)) {
|
|
324
|
+
const state = live?.sensors[name];
|
|
325
|
+
const liveSensor = live?.pack.sensors[name];
|
|
326
|
+
if (!shouldRun(sensor.fast ?? liveSensor?.fast ?? false, opts))
|
|
327
|
+
continue;
|
|
328
|
+
if (sensor.enabled === false) {
|
|
329
|
+
tasks.push(() => Promise.resolve({ name, status: 'skipped', errors: [], skipReason: 'disabled' }));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (!state || state.state !== 'certified') {
|
|
333
|
+
tasks.push(() => Promise.resolve({ name, status: 'inconclusive', errors: [], skipReason: state ? `${state.state}: ${state.reason}` : 'compatibility could not be revalidated' }));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (state.variantId !== sensor.variantId) {
|
|
337
|
+
tasks.push(() => Promise.resolve({ name, status: 'inconclusive', errors: [], skipReason: `variant-drift: manifest ${sensor.variantId}, live ${state.variantId ?? 'none'}; run \`awm sensors init\`` }));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const selected = liveSensor?.variants.find(candidate => candidate.id === state.variantId);
|
|
341
|
+
if (!selected) {
|
|
342
|
+
tasks.push(() => Promise.resolve({ name, status: 'inconclusive', errors: [], skipReason: 'variant-drift: selected live variant has no command; run `awm sensors init`' }));
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
// Variant IDs are stable selectors, not immutable command snapshots.
|
|
346
|
+
// Only the just re-resolved pack command is authorized to execute.
|
|
347
|
+
tasks.push(() => runV2Sensor(name, selected.command, (sensor.fast ?? liveSensor?.fast ?? false) ? DEFAULT_FAST_TIMEOUT : DEFAULT_SLOW_TIMEOUT, manifestDir, selected.formatter));
|
|
348
|
+
}
|
|
349
|
+
const sensors = await pooled(tasks, Math.min(MAX_CONCURRENCY, Math.max(1, tasks.length)));
|
|
350
|
+
return { sensors, overall: sensors.some(sensor => sensor.status === 'fail') ? 'fail' : sensors.some(sensor => sensor.status === 'inconclusive') ? 'not_certified' : sensors.length ? 'pass' : 'skipped' };
|
|
351
|
+
}
|
|
285
352
|
const drift = detectPackDrift(manifestDir, manifest);
|
|
286
353
|
const activeManifest = manifest; // el manifest COMITEADO es lo que se corre; `run` no lo reescribe
|
|
287
354
|
const cwd = manifestDir; // ejecutar sensores y baseline desde donde vive el manifest
|
|
@@ -382,6 +449,10 @@ async function runSensors(opts = {}) {
|
|
|
382
449
|
: results.length > 0 && results.every(r => r.status === 'skipped') ? 'skipped'
|
|
383
450
|
: results.length === 0 ? 'skipped'
|
|
384
451
|
: 'pass';
|
|
452
|
+
// Legacy commands retain operational compatibility but their unversioned,
|
|
453
|
+
// shell-backed contract can never certify a run.
|
|
454
|
+
if (parsedManifest.kind === 'legacy' && overall === 'pass')
|
|
455
|
+
overall = 'not_certified';
|
|
385
456
|
// Honest floor: a benign-green 'skipped' over a tree that clearly HAS a stack
|
|
386
457
|
// (indicators present) is a false green — the gate ran nothing real. Never green.
|
|
387
458
|
if (overall === 'skipped' && drift.detection.pack !== 'generic') {
|
|
@@ -7,6 +7,8 @@ exports.computeSensorStatus = computeSensorStatus;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const paths_1 = require("../../core/paths");
|
|
10
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
11
|
+
const live_1 = require("./compatibility/live");
|
|
10
12
|
/** First non-flag token after `npx` — the tool the command actually runs. */
|
|
11
13
|
function npxTool(parts) {
|
|
12
14
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -53,16 +55,57 @@ function checkCmd(cmd, cwd) {
|
|
|
53
55
|
}
|
|
54
56
|
return configCheck(parts, cwd) ?? { ok: true, detail: bin };
|
|
55
57
|
}
|
|
56
|
-
function computeSensorStatus(cwd = process.cwd()) {
|
|
58
|
+
async function computeSensorStatus(cwd = process.cwd()) {
|
|
57
59
|
const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
|
|
58
60
|
if (!fs_1.default.existsSync(manifestPath)) {
|
|
59
61
|
return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
|
|
60
62
|
}
|
|
61
63
|
let manifest;
|
|
62
64
|
try {
|
|
63
|
-
|
|
65
|
+
const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
66
|
+
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
67
|
+
if (parsed.kind === 'v2') {
|
|
68
|
+
const checks = {};
|
|
69
|
+
let live;
|
|
70
|
+
try {
|
|
71
|
+
live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const detail = `compatibility revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
75
|
+
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
76
|
+
checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' } : { ok: false, detail };
|
|
77
|
+
}
|
|
78
|
+
return { overall: 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
79
|
+
}
|
|
80
|
+
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
81
|
+
const state = live.sensors[name];
|
|
82
|
+
checks[name] = sensor.enabled === false
|
|
83
|
+
? { ok: true, detail: 'disabled' }
|
|
84
|
+
: !state
|
|
85
|
+
? { ok: false, detail: 'variant-drift: sensor no longer exists in the live pack; run `awm sensors init`' }
|
|
86
|
+
: state.variantId !== sensor.variantId
|
|
87
|
+
? { ok: false, detail: `variant-drift: manifest ${sensor.variantId}, live ${state.variantId ?? 'none'}; run \`awm sensors init\`` }
|
|
88
|
+
: state.state === 'certified'
|
|
89
|
+
? { ok: true, detail: `certified (${state.variantId})` }
|
|
90
|
+
: state.state === 'not-applicable'
|
|
91
|
+
? { ok: true, detail: 'not applicable' }
|
|
92
|
+
: { ok: false, detail: `${state.state}: ${state.reason}` };
|
|
93
|
+
}
|
|
94
|
+
return { overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'HEALTHY' : 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
95
|
+
}
|
|
96
|
+
manifest = parsed.pack;
|
|
64
97
|
}
|
|
65
98
|
catch {
|
|
99
|
+
// Preserve the historic diagnostic path for a hand-edited legacy manifest:
|
|
100
|
+
// malformed v2 is fail-closed, while a legacy `sensors:null` is surfaced as
|
|
101
|
+
// degraded rather than crashing the status command.
|
|
102
|
+
try {
|
|
103
|
+
const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
104
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw) && !('schemaVersion' in raw) && typeof raw.pack === 'string') {
|
|
105
|
+
return { overall: 'DEGRADED', pack: raw.pack, checks: {} };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { /* malformed JSON is not configured */ }
|
|
66
109
|
return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
|
|
67
110
|
}
|
|
68
111
|
const checks = {};
|
|
@@ -84,6 +127,7 @@ function computeSensorStatus(cwd = process.cwd()) {
|
|
|
84
127
|
if (Object.keys(manifest.sensors ?? {}).length === 0) {
|
|
85
128
|
return { overall: 'DEGRADED', pack: manifest.pack, checks };
|
|
86
129
|
}
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
// A legacy manifest may be operational (the checks are still useful), but it
|
|
131
|
+
// has no versioned/structured contract and must never present as certified.
|
|
132
|
+
return { overall: 'DEGRADED', pack: manifest.pack, checks };
|
|
89
133
|
}
|
|
@@ -77,6 +77,13 @@ async function runSyncCore(options, deps = {}) {
|
|
|
77
77
|
if (r.action === 'error')
|
|
78
78
|
console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
|
|
79
79
|
}
|
|
80
|
+
try {
|
|
81
|
+
(0, registries_1.assertSyncedRegistriesUsable)(syncResults);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error(picocolors_1.default.red(e.message));
|
|
85
|
+
return { code: 1, selectedAgents };
|
|
86
|
+
}
|
|
80
87
|
// Gate minCliVersion (WS-4) before pins (contract gates first — CONSTITUTION).
|
|
81
88
|
try {
|
|
82
89
|
(0, registries_1.assertRegistryGates)(d.verifyMinCliVersions());
|
package/dist/src/core/bundles.js
CHANGED
|
@@ -16,6 +16,41 @@ const registries_1 = require("./registries");
|
|
|
16
16
|
function catalogPath(contentDir) {
|
|
17
17
|
return path_1.default.join(contentDir, 'catalog.json');
|
|
18
18
|
}
|
|
19
|
+
function bundleManifestPath(contentDir, source) {
|
|
20
|
+
const root = path_1.default.resolve(contentDir);
|
|
21
|
+
const manifest = path_1.default.resolve(root, source, 'bundle.json');
|
|
22
|
+
const relative = path_1.default.relative(root, manifest);
|
|
23
|
+
if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
|
|
24
|
+
throw new Error(`Invalid bundle source "${source}" in ${catalogPath(contentDir)}: it must remain inside the registry content root.`);
|
|
25
|
+
}
|
|
26
|
+
let rootStat;
|
|
27
|
+
try {
|
|
28
|
+
rootStat = fs_1.default.lstatSync(root);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`Cannot inspect registry content root ${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
34
|
+
throw new Error(`Registry content root ${root} must be a regular directory`);
|
|
35
|
+
}
|
|
36
|
+
let current = root;
|
|
37
|
+
for (const segment of relative.split(path_1.default.sep)) {
|
|
38
|
+
current = path_1.default.join(current, segment);
|
|
39
|
+
let stat;
|
|
40
|
+
try {
|
|
41
|
+
stat = fs_1.default.lstatSync(current);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === 'ENOENT')
|
|
45
|
+
break;
|
|
46
|
+
throw new Error(`Cannot inspect registry bundle path ${current}: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
48
|
+
if (stat.isSymbolicLink()) {
|
|
49
|
+
throw new Error(`Registry bundle path ${current} must not be a symbolic link`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
19
54
|
/** Lee `catalog.json` de un registry.
|
|
20
55
|
*
|
|
21
56
|
* El contenido de un registry es INPUT NO CONFIABLE (un registry de equipo,
|
|
@@ -28,7 +63,7 @@ function catalogPath(contentDir) {
|
|
|
28
63
|
* dos eran los que faltaban. */
|
|
29
64
|
function readCatalog(contentDir) {
|
|
30
65
|
const file = catalogPath(contentDir);
|
|
31
|
-
if (!
|
|
66
|
+
if (!(0, registries_1.assertRegularRegistryFile)(file))
|
|
32
67
|
return [];
|
|
33
68
|
let parsed;
|
|
34
69
|
try {
|
|
@@ -74,8 +109,8 @@ function discoverBundles(contentDir) {
|
|
|
74
109
|
const entries = readCatalog(contentDir);
|
|
75
110
|
const bundles = [];
|
|
76
111
|
for (const entry of entries) {
|
|
77
|
-
const manifestPath =
|
|
78
|
-
if (!
|
|
112
|
+
const manifestPath = bundleManifestPath(contentDir, entry.source);
|
|
113
|
+
if (!(0, registries_1.assertRegularRegistryFile)(manifestPath))
|
|
79
114
|
continue;
|
|
80
115
|
let raw;
|
|
81
116
|
try {
|
|
@@ -65,8 +65,8 @@ exports.defaultActions = {
|
|
|
65
65
|
method: o.method,
|
|
66
66
|
contentDir: o.contentDir,
|
|
67
67
|
}),
|
|
68
|
-
initSensors: (o) => {
|
|
69
|
-
const result = (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
|
|
68
|
+
initSensors: async (o) => {
|
|
69
|
+
const result = await (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
|
|
70
70
|
// `unavailablePack`/`manifest` viajan a proposito: `stepSensors` los reporta.
|
|
71
71
|
// Este wrapper proyectaba solo `detection`, asi que cualquier hallazgo nuevo de
|
|
72
72
|
// initSensors moria aca en silencio antes de llegar al step que lo muestra.
|
|
@@ -216,6 +216,9 @@ function stepDevCore(d) {
|
|
|
216
216
|
// `awm doctor` decia `healthy`. Su propio `globalUnsupportedReason` ya dice
|
|
217
217
|
// que hay que instalar por proyecto, asi que eso es lo que se hace.
|
|
218
218
|
const localOnly = (0, providers_1.providerFor)(d.agent).skill.global === null;
|
|
219
|
+
if (localOnly && d.machineOnly) {
|
|
220
|
+
return ok('machine.devCore', 'machine', 'skipped', `${(0, providers_1.providerFor)(d.agent).label} provides skills at project scope — deferred until project initialization`);
|
|
221
|
+
}
|
|
219
222
|
for (const bundleName of toInstall) {
|
|
220
223
|
d.actions.installBundle({
|
|
221
224
|
bundleName,
|
|
@@ -316,13 +319,13 @@ function stepActivation(d) {
|
|
|
316
319
|
return ok('project.activation', 'project', 'applied');
|
|
317
320
|
}
|
|
318
321
|
/** Step 7 – Initialize the sensor manifest if absent. */
|
|
319
|
-
function stepSensors(d) {
|
|
322
|
+
async function stepSensors(d) {
|
|
320
323
|
const proj = d.ctx.project;
|
|
321
324
|
if (!proj)
|
|
322
325
|
return ok('project.sensors', 'project', 'skipped', 'no project');
|
|
323
326
|
if (proj.sensors.present)
|
|
324
327
|
return ok('project.sensors', 'project', 'skipped');
|
|
325
|
-
const res = d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
328
|
+
const res = await d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
326
329
|
// A registry without the detected pack yields a fallback (or empty) manifest. The
|
|
327
330
|
// step still succeeded — a manifest was written — but reporting a bare 'applied'
|
|
328
331
|
// would let `awm init` hand back a quality gate that measures less than the
|
|
@@ -0,0 +1,228 @@
|
|
|
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.DEFAULT_LEDGER_SCAN_LIMITS = void 0;
|
|
7
|
+
exports.scanProjectLedgers = scanProjectLedgers;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
exports.DEFAULT_LEDGER_SCAN_LIMITS = {
|
|
12
|
+
maxFiles: 256,
|
|
13
|
+
maxFileBytes: 4 * 1024 * 1024,
|
|
14
|
+
maxEntries: 20_000,
|
|
15
|
+
maxLineBytes: 64 * 1024,
|
|
16
|
+
maxJsonDepth: 16,
|
|
17
|
+
maxRefsPerClass: 128,
|
|
18
|
+
};
|
|
19
|
+
function isWithin(root, candidate) {
|
|
20
|
+
const relative = path_1.default.relative(root, candidate);
|
|
21
|
+
return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
|
|
22
|
+
}
|
|
23
|
+
function assertDirectoryInside(root, directory) {
|
|
24
|
+
if (!fs_1.default.existsSync(directory))
|
|
25
|
+
return false;
|
|
26
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
27
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
28
|
+
throw new Error(`ledger directory is unsafe: ${directory}`);
|
|
29
|
+
return isWithin(root, fs_1.default.realpathSync(directory));
|
|
30
|
+
}
|
|
31
|
+
function isJsonDepthWithinLimit(raw, maxDepth) {
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let quoted = false;
|
|
34
|
+
let escaped = false;
|
|
35
|
+
for (const char of raw) {
|
|
36
|
+
if (quoted) {
|
|
37
|
+
if (escaped)
|
|
38
|
+
escaped = false;
|
|
39
|
+
else if (char === '\\')
|
|
40
|
+
escaped = true;
|
|
41
|
+
else if (char === '"')
|
|
42
|
+
quoted = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '"')
|
|
46
|
+
quoted = true;
|
|
47
|
+
else if (char === '{' || char === '[') {
|
|
48
|
+
depth += 1;
|
|
49
|
+
if (depth > maxDepth)
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
else if (char === '}' || char === ']')
|
|
53
|
+
depth -= 1;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function boundedLimits(overrides) {
|
|
58
|
+
if (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides)) {
|
|
59
|
+
throw new Error('ledger scan limits must be an object');
|
|
60
|
+
}
|
|
61
|
+
const limits = { ...exports.DEFAULT_LEDGER_SCAN_LIMITS, ...overrides };
|
|
62
|
+
for (const value of Object.values(limits)) {
|
|
63
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
64
|
+
throw new Error('ledger scan limits must be positive safe integers');
|
|
65
|
+
}
|
|
66
|
+
return limits;
|
|
67
|
+
}
|
|
68
|
+
function relativeSource(root, target, line) {
|
|
69
|
+
return `${relativePath(root, target)}:${line}`;
|
|
70
|
+
}
|
|
71
|
+
function relativePath(root, target) {
|
|
72
|
+
return path_1.default.relative(root, target).split(path_1.default.sep).join('/');
|
|
73
|
+
}
|
|
74
|
+
function compareCandidates(a, b) {
|
|
75
|
+
return a.sourcePath < b.sourcePath ? -1 : a.sourcePath > b.sourcePath ? 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Reads one global bounded window across the direct ledger directories, then
|
|
79
|
+
* sorts only the retained candidates. This deliberately does not claim to
|
|
80
|
+
* find a globally lexical prefix in an unbounded directory: the extra
|
|
81
|
+
* candidate is a truncation witness, and no later candidate is opened or
|
|
82
|
+
* retained from either active or archive storage.
|
|
83
|
+
*/
|
|
84
|
+
function collectBoundedCandidates(root, directories, maxFiles) {
|
|
85
|
+
const candidates = [];
|
|
86
|
+
const capacity = maxFiles + 1;
|
|
87
|
+
let truncated = false;
|
|
88
|
+
for (const { directory, archive } of directories) {
|
|
89
|
+
if (candidates.length >= capacity)
|
|
90
|
+
break;
|
|
91
|
+
const handle = fs_1.default.opendirSync(directory);
|
|
92
|
+
try {
|
|
93
|
+
let item;
|
|
94
|
+
while ((item = handle.readSync()) !== null) {
|
|
95
|
+
if (!item.name.endsWith('.jsonl'))
|
|
96
|
+
continue;
|
|
97
|
+
const target = path_1.default.join(directory, item.name);
|
|
98
|
+
candidates.push({ isArchive: archive, target, sourcePath: relativePath(root, target) });
|
|
99
|
+
if (candidates.length === capacity) {
|
|
100
|
+
truncated = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
handle.closeSync();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
candidates.sort(compareCandidates);
|
|
110
|
+
return { candidates, truncated: truncated || candidates.length > maxFiles };
|
|
111
|
+
}
|
|
112
|
+
/** Reads only direct .jsonl children of active/archive ledger roots; never follows links. */
|
|
113
|
+
function scanProjectLedgers(projectRoot, overrides = {}) {
|
|
114
|
+
if (typeof projectRoot !== 'string' || !path_1.default.isAbsolute(projectRoot))
|
|
115
|
+
throw new Error('projectRoot must be an absolute path');
|
|
116
|
+
const limits = boundedLimits(overrides);
|
|
117
|
+
const root = fs_1.default.realpathSync(projectRoot);
|
|
118
|
+
const ledgerRoot = path_1.default.join(root, '.awm', 'ledger');
|
|
119
|
+
const sources = {
|
|
120
|
+
activeFiles: 0, archivedFiles: 0, validEntries: 0, validFindings: 0, skippedFindings: 0, skippedByReason: {},
|
|
121
|
+
};
|
|
122
|
+
const entries = [];
|
|
123
|
+
const skip = (reason) => {
|
|
124
|
+
sources.skippedFindings += 1;
|
|
125
|
+
sources.skippedByReason[reason] = (sources.skippedByReason[reason] ?? 0) + 1;
|
|
126
|
+
};
|
|
127
|
+
if (!fs_1.default.existsSync(ledgerRoot))
|
|
128
|
+
return { entries, sources, omittedEvidenceRefs: 0 };
|
|
129
|
+
if (!assertDirectoryInside(root, path_1.default.join(root, '.awm')) || !assertDirectoryInside(root, ledgerRoot)) {
|
|
130
|
+
throw new Error('ledger directory escapes project root');
|
|
131
|
+
}
|
|
132
|
+
const directories = [{ directory: ledgerRoot, archive: false }];
|
|
133
|
+
const archive = path_1.default.join(ledgerRoot, 'archive');
|
|
134
|
+
if (fs_1.default.existsSync(archive)) {
|
|
135
|
+
if (!assertDirectoryInside(root, archive))
|
|
136
|
+
throw new Error('ledger archive directory escapes project root');
|
|
137
|
+
directories.push({ directory: archive, archive: true });
|
|
138
|
+
}
|
|
139
|
+
let filesSeen = 0;
|
|
140
|
+
let linesSeen = 0;
|
|
141
|
+
let entryLimitReached = false;
|
|
142
|
+
let omittedEvidenceRefs = 0;
|
|
143
|
+
const evidenceRefsByClass = new Map();
|
|
144
|
+
const collected = collectBoundedCandidates(root, directories, limits.maxFiles);
|
|
145
|
+
for (const { isArchive, target } of collected.candidates.slice(0, limits.maxFiles)) {
|
|
146
|
+
if (entryLimitReached)
|
|
147
|
+
break;
|
|
148
|
+
filesSeen += 1;
|
|
149
|
+
const stat = fs_1.default.lstatSync(target);
|
|
150
|
+
if (stat.isSymbolicLink()) {
|
|
151
|
+
skip('symlink-entry');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!stat.isFile()) {
|
|
155
|
+
skip('nonregular-entry');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const real = fs_1.default.realpathSync(target);
|
|
159
|
+
if (!isWithin(root, real)) {
|
|
160
|
+
skip('path-escape');
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (stat.size > limits.maxFileBytes) {
|
|
164
|
+
skip('file-too-large');
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (isArchive)
|
|
168
|
+
sources.archivedFiles += 1;
|
|
169
|
+
else
|
|
170
|
+
sources.activeFiles += 1;
|
|
171
|
+
const lines = fs_1.default.readFileSync(target, 'utf-8').split('\n');
|
|
172
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
173
|
+
const raw = lines[index];
|
|
174
|
+
if (!raw.trim())
|
|
175
|
+
continue;
|
|
176
|
+
if (linesSeen >= limits.maxEntries) {
|
|
177
|
+
skip('entry-limit');
|
|
178
|
+
entryLimitReached = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
linesSeen += 1;
|
|
182
|
+
if (Buffer.byteLength(raw, 'utf-8') > limits.maxLineBytes) {
|
|
183
|
+
skip('line-too-large');
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!isJsonDepthWithinLimit(raw, limits.maxJsonDepth)) {
|
|
187
|
+
skip('json-too-deep');
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let value;
|
|
191
|
+
try {
|
|
192
|
+
value = JSON.parse(raw);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
skip('invalid-json');
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const source = relativeSource(root, target, index + 1);
|
|
199
|
+
const parsed = (0, types_1.parseLedgerEntry)(value, source);
|
|
200
|
+
if (!parsed.ok) {
|
|
201
|
+
skip(parsed.reason);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
sources.validEntries += 1;
|
|
205
|
+
if (parsed.entry.polarity === 'finding') {
|
|
206
|
+
sources.validFindings += 1;
|
|
207
|
+
const defectClass = parsed.entry.defectClass ?? 'unclassified';
|
|
208
|
+
const count = evidenceRefsByClass.get(defectClass) ?? 0;
|
|
209
|
+
const evidenceRef = count < limits.maxRefsPerClass ? source : null;
|
|
210
|
+
if (evidenceRef === null) {
|
|
211
|
+
omittedEvidenceRefs += 1;
|
|
212
|
+
// The finding remains available for recurrence analysis, but
|
|
213
|
+
// its public evidence is deliberately omitted. Record the
|
|
214
|
+
// truncation through the same typed source contract as every
|
|
215
|
+
// other scan bound so downstream renderers cannot call it a
|
|
216
|
+
// complete report.
|
|
217
|
+
skip('evidence-ref-limit');
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
evidenceRefsByClass.set(defectClass, count + 1);
|
|
221
|
+
entries.push({ entry: parsed.entry, source, evidenceRef });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!entryLimitReached && collected.truncated)
|
|
226
|
+
skip('file-limit');
|
|
227
|
+
return { entries, sources, omittedEvidenceRefs };
|
|
228
|
+
}
|