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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCompatibilityProbe = runCompatibilityProbe;
|
|
4
|
+
const exec_1 = require("../exec");
|
|
5
|
+
const KINDS = new Set(['version', 'eslint-print-config', 'typescript-show-config', 'semgrep-validate', 'package-script-present', 'config-present']);
|
|
6
|
+
function commandFor(kind, evidence) {
|
|
7
|
+
const executable = evidence.toolExecutable ?? (kind.startsWith('typescript') ? 'tsc' : kind.startsWith('eslint') ? 'eslint' : kind.startsWith('semgrep') ? 'semgrep' : 'node');
|
|
8
|
+
// A tool version certified from local package metadata must be probed through
|
|
9
|
+
// the same project installation. Only the Node runtime is intentionally PATH
|
|
10
|
+
// resolved: it is runtime evidence, not a package-local tool assertion.
|
|
11
|
+
const resolution = executable === 'node'
|
|
12
|
+
? 'path'
|
|
13
|
+
: kind === 'semgrep-validate'
|
|
14
|
+
? 'python-environment'
|
|
15
|
+
: 'node-modules-bin';
|
|
16
|
+
if (kind === 'package-script-present')
|
|
17
|
+
return null;
|
|
18
|
+
if (kind === 'config-present')
|
|
19
|
+
return null;
|
|
20
|
+
if (kind === 'version')
|
|
21
|
+
return { executable, resolution, args: ['--version'] };
|
|
22
|
+
if (kind === 'eslint-print-config')
|
|
23
|
+
return { executable, resolution, args: ['--print-config', evidence.configFiles?.[0] ?? 'package.json'] };
|
|
24
|
+
if (kind === 'typescript-show-config')
|
|
25
|
+
return { executable, resolution, args: ['--showConfig'] };
|
|
26
|
+
return { executable, resolution, args: ['--validate'] };
|
|
27
|
+
}
|
|
28
|
+
/** Executes only the closed probe enum. Raw output is intentionally discarded. */
|
|
29
|
+
async function runCompatibilityProbe(probe, evidence, executor = exec_1.runStructuredCommand) {
|
|
30
|
+
if (!probe || typeof probe !== 'object' || !KINDS.has(probe.kind))
|
|
31
|
+
throw new Error('compatibility probe kind must be allowed');
|
|
32
|
+
if (!evidence || typeof evidence.cwd !== 'string' || evidence.cwd.trim() === '')
|
|
33
|
+
throw new Error('probe evidence requires cwd');
|
|
34
|
+
if (probe.kind === 'package-script-present')
|
|
35
|
+
return { status: (evidence.scripts?.length ?? 0) > 0 ? 'matched' : 'not-matched', reason: 'package-script' };
|
|
36
|
+
if (probe.kind === 'config-present')
|
|
37
|
+
return { status: (evidence.configFiles?.length ?? 0) > 0 ? 'matched' : 'not-matched', reason: 'config-file' };
|
|
38
|
+
try {
|
|
39
|
+
const result = await executor(commandFor(probe.kind, evidence), { cwd: evidence.cwd, timeout: 5_000, maxBuffer: 64 * 1024 });
|
|
40
|
+
if (result.timedOut || result.overflowed || result.spawnError)
|
|
41
|
+
return { status: 'unverifiable', reason: result.timedOut ? 'probe-timeout' : result.overflowed ? 'probe-output-limit' : 'probe-spawn-error' };
|
|
42
|
+
return result.code === 0 ? { status: 'matched', reason: 'probe-exit-zero' } : { status: 'not-matched', reason: 'probe-exit-nonzero' };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { status: 'unverifiable', reason: 'probe-exception' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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.resolveSensorCompatibility = resolveSensorCompatibility;
|
|
7
|
+
exports.resolveProjectCompatibility = resolveProjectCompatibility;
|
|
8
|
+
const semver_1 = __importDefault(require("semver"));
|
|
9
|
+
const manifest_1 = require("./manifest");
|
|
10
|
+
function result(state, reason, variant, evidence) {
|
|
11
|
+
const toolVersion = (variant ? toolFor(variant, evidence) : evidence.toolVersion) ?? null;
|
|
12
|
+
const runtimeVersion = (variant ? runtimeFor(variant, evidence) : evidence.runtimeVersion) ?? null;
|
|
13
|
+
const refs = [];
|
|
14
|
+
for (const candidate of (evidence.paths ?? []).slice(0, 32)) {
|
|
15
|
+
if (typeof candidate === 'string' && candidate.length <= 240 && !candidate.startsWith('/') && !candidate.includes('\\') && !candidate.split('/').some(part => part === '' || part === '.' || part === '..'))
|
|
16
|
+
refs.push({ kind: 'project-path', status: 'present', path: candidate });
|
|
17
|
+
}
|
|
18
|
+
if (evidence.packageManagerConflict)
|
|
19
|
+
refs.push({ kind: 'package-manager', status: 'conflict' });
|
|
20
|
+
if (variant && validVersion(toolVersion))
|
|
21
|
+
refs.push({ kind: 'tool-version', status: toolVersion });
|
|
22
|
+
if (variant && validVersion(runtimeVersion))
|
|
23
|
+
refs.push({ kind: 'runtime-version', status: runtimeVersion });
|
|
24
|
+
if (evidence.probe?.status === 'matched' || evidence.probe?.status === 'not-matched' || evidence.probe?.status === 'unverifiable')
|
|
25
|
+
refs.push({ kind: 'probe', status: evidence.probe.status });
|
|
26
|
+
return { state, reason, variantId: variant?.id ?? null, toolVersion, runtimeVersion, certifiedRange: variant?.certifiedRange ?? null, evidence: refs.slice(0, 32) };
|
|
27
|
+
}
|
|
28
|
+
function applies(sensor, evidence) {
|
|
29
|
+
if (evidence.applicable === false)
|
|
30
|
+
return false;
|
|
31
|
+
const paths = new Set(evidence.paths ?? []);
|
|
32
|
+
const rule = sensor.applicability;
|
|
33
|
+
return (!rule.allFiles || rule.allFiles.every(file => paths.has(file))) && (!rule.anyFiles || rule.anyFiles.some(file => paths.has(file)));
|
|
34
|
+
}
|
|
35
|
+
function specificity(variant) {
|
|
36
|
+
return variant.requirements.toolRange.length + variant.requirements.runtimeRange.length + (variant.requirements.configFiles?.length ?? 0);
|
|
37
|
+
}
|
|
38
|
+
function validVersion(value) { return typeof value === 'string' && semver_1.default.valid(value) !== null; }
|
|
39
|
+
function toolFor(variant, evidence) {
|
|
40
|
+
return evidence.toolVersions === undefined ? evidence.toolVersion : evidence.toolVersions[variant.requirements.tool];
|
|
41
|
+
}
|
|
42
|
+
function runtimeFor(variant, evidence) {
|
|
43
|
+
return evidence.runtimeVersions === undefined ? evidence.runtimeVersion : evidence.runtimeVersions[variant.requirements.runtime];
|
|
44
|
+
}
|
|
45
|
+
/** Pure precedence resolver. It consumes discovered evidence and neither probes nor executes commands. */
|
|
46
|
+
function resolveSensorCompatibility(sensor, evidence, context) {
|
|
47
|
+
if (!sensor || typeof sensor !== 'object' || !evidence || typeof evidence !== 'object')
|
|
48
|
+
throw new Error('sensor and discovered evidence are required');
|
|
49
|
+
if (!context || typeof context.pack !== 'string' || !context.pack.trim() || typeof context.sensor !== 'string' || !context.sensor.trim())
|
|
50
|
+
throw new Error('pack and sensor identity are required for compatibility resolution');
|
|
51
|
+
if (!('variants' in sensor))
|
|
52
|
+
return (0, manifest_1.legacyCompatibility)('legacy pack without schemaVersion');
|
|
53
|
+
const v2 = sensor;
|
|
54
|
+
if (!Array.isArray(v2.variants) || !v2.applicability)
|
|
55
|
+
throw new Error('v2 sensor must declare variants and applicability');
|
|
56
|
+
if (!applies(v2, evidence))
|
|
57
|
+
return result('not-applicable', 'applicability-not-met', null, evidence);
|
|
58
|
+
if (evidence.packageManagerConflict)
|
|
59
|
+
return result('unverifiable', 'package-manager-conflict', null, evidence);
|
|
60
|
+
const availableTools = v2.variants.map(variant => toolFor(variant, evidence));
|
|
61
|
+
if (availableTools.every(version => version === null || version === undefined))
|
|
62
|
+
return result('missing-tool', 'tool-not-found', null, evidence);
|
|
63
|
+
const operational = v2.variants.filter(variant => validVersion(toolFor(variant, evidence)) && validVersion(runtimeFor(variant, evidence)));
|
|
64
|
+
if (operational.length === 0)
|
|
65
|
+
return result('unverifiable', 'invalid-or-missing-version-evidence', null, evidence);
|
|
66
|
+
const matches = operational.filter(variant => semver_1.default.satisfies(toolFor(variant, evidence), variant.requirements.toolRange) && semver_1.default.satisfies(runtimeFor(variant, evidence), variant.requirements.runtimeRange));
|
|
67
|
+
if (matches.length === 0)
|
|
68
|
+
return result('incompatible', 'no-operational-variant', null, evidence);
|
|
69
|
+
matches.sort((a, b) => b.priority - a.priority || specificity(b) - specificity(a) || a.id.localeCompare(b.id));
|
|
70
|
+
const best = matches[0];
|
|
71
|
+
const tied = matches.filter(variant => variant.priority === best.priority && specificity(variant) === specificity(best));
|
|
72
|
+
if (tied.length !== 1) {
|
|
73
|
+
const matching = tied.sort((a, b) => a.id.localeCompare(b.id)).map(variant => `${variant.id} (tool ${variant.requirements.toolRange}, runtime ${variant.requirements.runtimeRange})`).join(', ');
|
|
74
|
+
throw new Error(`ambiguous sensor variants in pack "${context.pack}" sensor "${context.sensor}": ${matching}`);
|
|
75
|
+
}
|
|
76
|
+
if (evidence.probe?.status !== 'matched')
|
|
77
|
+
return result('unverifiable', evidence.probe?.status === 'not-matched' ? 'probe-not-matched' : 'probe-inconclusive', best, evidence);
|
|
78
|
+
return semver_1.default.satisfies(toolFor(best, evidence), best.certifiedRange)
|
|
79
|
+
? result('certified', 'range-and-probe', best, evidence)
|
|
80
|
+
: result('compatible-unverified', 'operational-range-and-probe', best, evidence);
|
|
81
|
+
}
|
|
82
|
+
/** Adapt bounded local project discovery into one source-aware resolution per sensor. */
|
|
83
|
+
function resolveProjectCompatibility(pack, evidence) {
|
|
84
|
+
if (!pack || typeof pack !== 'object' || typeof pack.name !== 'string')
|
|
85
|
+
throw new Error('pack must be a parsed sensor pack');
|
|
86
|
+
const sensors = {};
|
|
87
|
+
if (!('schemaVersion' in pack)) {
|
|
88
|
+
for (const name of Object.keys(pack.sensors))
|
|
89
|
+
sensors[name] = (0, manifest_1.legacyCompatibility)('legacy pack without schemaVersion');
|
|
90
|
+
return { sensors };
|
|
91
|
+
}
|
|
92
|
+
for (const [name, sensor] of Object.entries(pack.sensors)) {
|
|
93
|
+
sensors[name] = resolveSensorCompatibility(sensor, evidence, { pack: pack.name, sensor: name });
|
|
94
|
+
}
|
|
95
|
+
return { sensors };
|
|
96
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MAX_COVERAGE_FILE_BYTES = exports.COVERAGE_SCHEMA_VERSION = void 0;
|
|
4
4
|
exports.parseCoverageContract = parseCoverageContract;
|
|
5
|
-
exports.parseCoverageManifest = parseCoverageManifest;
|
|
6
5
|
exports.COVERAGE_SCHEMA_VERSION = 1;
|
|
7
6
|
exports.MAX_COVERAGE_FILE_BYTES = 1024 * 1024;
|
|
8
7
|
const COVERAGE_EVIDENCE_PATHS = new Set([
|
|
@@ -123,52 +122,3 @@ function parseCoverageContract(input, source) {
|
|
|
123
122
|
}
|
|
124
123
|
return { schemaVersion: exports.COVERAGE_SCHEMA_VERSION, classes };
|
|
125
124
|
}
|
|
126
|
-
function manifestString(value, source, location) {
|
|
127
|
-
return nonEmptyString(value, source, location);
|
|
128
|
-
}
|
|
129
|
-
function parseManifestSensor(input, source, location) {
|
|
130
|
-
const value = record(input, source, location);
|
|
131
|
-
fields(value, ['cmd', 'fast', 'enabled', 'timeout', 'changedCmd', 'changedExtensions', 'formatter'], source, location);
|
|
132
|
-
const sensor = {};
|
|
133
|
-
if ('cmd' in value)
|
|
134
|
-
sensor.cmd = manifestString(value.cmd, source, `${location}.cmd`);
|
|
135
|
-
if ('fast' in value) {
|
|
136
|
-
if (typeof value.fast !== 'boolean')
|
|
137
|
-
invalid(source, `${location}.fast must be a boolean`);
|
|
138
|
-
sensor.fast = value.fast;
|
|
139
|
-
}
|
|
140
|
-
if ('enabled' in value) {
|
|
141
|
-
if (typeof value.enabled !== 'boolean')
|
|
142
|
-
invalid(source, `${location}.enabled must be a boolean`);
|
|
143
|
-
sensor.enabled = value.enabled;
|
|
144
|
-
}
|
|
145
|
-
if ('timeout' in value) {
|
|
146
|
-
if (typeof value.timeout !== 'number' || !Number.isSafeInteger(value.timeout) || value.timeout <= 0)
|
|
147
|
-
invalid(source, `${location}.timeout must be a positive safe integer`);
|
|
148
|
-
sensor.timeout = value.timeout;
|
|
149
|
-
}
|
|
150
|
-
if ('changedCmd' in value)
|
|
151
|
-
sensor.changedCmd = manifestString(value.changedCmd, source, `${location}.changedCmd`);
|
|
152
|
-
if ('changedExtensions' in value)
|
|
153
|
-
sensor.changedExtensions = stringArray(value.changedExtensions, source, `${location}.changedExtensions`, true);
|
|
154
|
-
if ('formatter' in value)
|
|
155
|
-
sensor.formatter = manifestString(value.formatter, source, `${location}.formatter`);
|
|
156
|
-
return sensor;
|
|
157
|
-
}
|
|
158
|
-
function parseCoverageManifest(input, source) {
|
|
159
|
-
const value = record(input, source, 'manifest root');
|
|
160
|
-
fields(value, ['pack', 'sensors', 'concurrency'], source, 'manifest root');
|
|
161
|
-
const pack = safeName(value.pack, source, 'manifest.pack');
|
|
162
|
-
const sensorsInput = record(value.sensors, source, 'manifest.sensors');
|
|
163
|
-
const sensors = {};
|
|
164
|
-
for (const name of Object.keys(sensorsInput)) {
|
|
165
|
-
sensors[safeName(name, source, 'manifest sensor name')] = parseManifestSensor(sensorsInput[name], source, `manifest.sensors.${name}`);
|
|
166
|
-
}
|
|
167
|
-
const manifest = { pack, sensors };
|
|
168
|
-
if ('concurrency' in value) {
|
|
169
|
-
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
170
|
-
invalid(source, 'manifest.concurrency must be a positive safe integer');
|
|
171
|
-
manifest.concurrency = value.concurrency;
|
|
172
|
-
}
|
|
173
|
-
return manifest;
|
|
174
|
-
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outcomeFor = outcomeFor;
|
|
4
|
+
exports.evaluateEmpiricalCoverage = evaluateEmpiricalCoverage;
|
|
5
|
+
const cluster_1 = require("../../../core/ledger/cluster");
|
|
6
|
+
const ANSI = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
7
|
+
const OSC = /\x1B\][\s\S]*?(?:\x07|\x1B\\)/g;
|
|
8
|
+
const CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
9
|
+
const severityRank = { blocker: 4, important: 3, minor: 2, info: 1 };
|
|
10
|
+
const relativeLine = /^(?!\/)(?!.*(?:^|\/)\.\.?\/)[A-Za-z0-9._@+~=-]+(?:\/[A-Za-z0-9._@+~=-]+)*:[1-9][0-9]*$/;
|
|
11
|
+
const prRef = /^PR #[1-9][0-9]*$/;
|
|
12
|
+
const hashRef = /^[a-f0-9]{7,64}$/i;
|
|
13
|
+
function safeRef(value) {
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return null;
|
|
16
|
+
const cleaned = value.replace(OSC, '').replace(ANSI, '').replace(CONTROLS, '').trim().slice(0, 256);
|
|
17
|
+
return relativeLine.test(cleaned) || prRef.test(cleaned) || hashRef.test(cleaned) ? cleaned : null;
|
|
18
|
+
}
|
|
19
|
+
function refsFor(entries, scanned) {
|
|
20
|
+
const refs = new Set();
|
|
21
|
+
let omitted = 0;
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const scannedEntry = scanned.get(entry);
|
|
24
|
+
if (!scannedEntry || scannedEntry.evidenceRef === null)
|
|
25
|
+
continue;
|
|
26
|
+
const ref = safeRef(scannedEntry.evidenceRef);
|
|
27
|
+
if (ref === null)
|
|
28
|
+
omitted += 1;
|
|
29
|
+
else
|
|
30
|
+
refs.add(ref);
|
|
31
|
+
}
|
|
32
|
+
return { refs: [...refs].sort((a, b) => a.localeCompare(b)), omitted };
|
|
33
|
+
}
|
|
34
|
+
function signaturesFor(signatures) {
|
|
35
|
+
const safe = new Set();
|
|
36
|
+
let omitted = 0;
|
|
37
|
+
for (const signature of signatures) {
|
|
38
|
+
const publicSignature = safeRef(signature);
|
|
39
|
+
if (publicSignature === null)
|
|
40
|
+
omitted += 1;
|
|
41
|
+
else
|
|
42
|
+
safe.add(publicSignature);
|
|
43
|
+
}
|
|
44
|
+
return { signatures: [...safe].sort((a, b) => a.localeCompare(b)), omitted };
|
|
45
|
+
}
|
|
46
|
+
function maxSeverity(entries) {
|
|
47
|
+
return entries.reduce((best, entry) => severityRank[entry.severity] > severityRank[best] ? entry.severity : best, 'info');
|
|
48
|
+
}
|
|
49
|
+
function outcomeFor(staticState, hasEvidence, staticAvailability = 'available') {
|
|
50
|
+
if (typeof hasEvidence !== 'boolean')
|
|
51
|
+
throw new Error('outcomeFor: hasEvidence must be boolean');
|
|
52
|
+
if (staticAvailability !== 'available' && staticAvailability !== 'unavailable')
|
|
53
|
+
throw new Error('outcomeFor: invalid static availability');
|
|
54
|
+
if (staticAvailability === 'unavailable')
|
|
55
|
+
return 'coverage-unverifiable';
|
|
56
|
+
if (!hasEvidence || staticState === 'unverifiable' || staticState === 'compatible-unverified')
|
|
57
|
+
return 'coverage-unverifiable';
|
|
58
|
+
if (staticState === undefined)
|
|
59
|
+
return 'unmapped-class';
|
|
60
|
+
if (staticState === 'covered')
|
|
61
|
+
return 'covered-by-sensor';
|
|
62
|
+
if (staticState === 'not-applicable')
|
|
63
|
+
return 'applicability-contradiction';
|
|
64
|
+
if (staticState === 'missing' || staticState === 'incompatible' || staticState === 'missing-tool')
|
|
65
|
+
return 'gap';
|
|
66
|
+
throw new Error('outcomeFor: invalid static state');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Converts bounded ledger findings into a public aggregate. Defect classes are
|
|
70
|
+
* the sole grouping boundary: no description or signature is used to classify
|
|
71
|
+
* an entry, and those private fields never cross this module's output.
|
|
72
|
+
*/
|
|
73
|
+
function evaluateEmpiricalCoverage(scan, staticStates, min, staticAvailability = 'available') {
|
|
74
|
+
if (!scan || typeof scan !== 'object' || !Array.isArray(scan.entries) || !scan.sources || typeof scan.omittedEvidenceRefs !== 'number') {
|
|
75
|
+
throw new Error('evaluateEmpiricalCoverage: invalid ledger scan');
|
|
76
|
+
}
|
|
77
|
+
if (!Number.isSafeInteger(min) || min < 1)
|
|
78
|
+
throw new Error('evaluateEmpiricalCoverage: min must be a positive safe integer');
|
|
79
|
+
if (staticAvailability !== 'available' && staticAvailability !== 'unavailable')
|
|
80
|
+
throw new Error('evaluateEmpiricalCoverage: invalid static availability');
|
|
81
|
+
const scanned = new Map(scan.entries.map(item => [item.entry, item]));
|
|
82
|
+
const typed = new Map();
|
|
83
|
+
const unclassified = [];
|
|
84
|
+
for (const item of scan.entries) {
|
|
85
|
+
if (item.entry.polarity !== 'finding')
|
|
86
|
+
continue;
|
|
87
|
+
if (item.entry.defectClass === undefined)
|
|
88
|
+
unclassified.push(item.entry);
|
|
89
|
+
else
|
|
90
|
+
typed.set(item.entry.defectClass, [...(typed.get(item.entry.defectClass) ?? []), item.entry]);
|
|
91
|
+
}
|
|
92
|
+
const classes = [];
|
|
93
|
+
for (const [defectClass, entries] of [...typed.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
94
|
+
const evidence = refsFor(entries, scanned);
|
|
95
|
+
const clusters = (0, cluster_1.clusterEntries)(entries, 1).map((cluster) => {
|
|
96
|
+
const signatures = signaturesFor(cluster.signatures);
|
|
97
|
+
const clusterEvidence = refsFor(cluster.entries, scanned);
|
|
98
|
+
return {
|
|
99
|
+
occurrences: cluster.count,
|
|
100
|
+
recurrent: cluster.count >= min,
|
|
101
|
+
severity: maxSeverity(cluster.entries),
|
|
102
|
+
kind: cluster.kind,
|
|
103
|
+
signatures: signatures.signatures,
|
|
104
|
+
omittedSignatures: signatures.omitted,
|
|
105
|
+
evidenceRefs: clusterEvidence.refs,
|
|
106
|
+
omittedEvidenceRefs: clusterEvidence.omitted,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
classes.push({ defectClass, occurrences: entries.length, recurrent: clusters.some((cluster) => cluster.recurrent),
|
|
110
|
+
severity: maxSeverity(entries), outcome: outcomeFor(staticStates[defectClass], entries.length > 0, staticAvailability),
|
|
111
|
+
evidenceRefs: evidence.refs, omittedEvidenceRefs: evidence.omitted, clusters });
|
|
112
|
+
}
|
|
113
|
+
// Stable public order: recurrence emphasis, count, then class ID. The
|
|
114
|
+
// cluster helper already supplies its own deterministic inner ordering.
|
|
115
|
+
classes.sort((a, b) => Number(b.recurrent) - Number(a.recurrent) || b.occurrences - a.occurrences
|
|
116
|
+
|| a.defectClass.localeCompare(b.defectClass));
|
|
117
|
+
const unclassifiedEvidence = refsFor(unclassified, scanned);
|
|
118
|
+
const retained = classes.reduce((total, item) => total + item.occurrences, 0) + unclassified.length;
|
|
119
|
+
const omittedEvidenceRefs = scan.omittedEvidenceRefs
|
|
120
|
+
+ classes.reduce((total, item) => total + item.omittedEvidenceRefs, 0)
|
|
121
|
+
+ unclassifiedEvidence.omitted;
|
|
122
|
+
const skipped = scan.sources.skippedFindings > 0 || omittedEvidenceRefs > 0 || unclassified.length > 0;
|
|
123
|
+
return {
|
|
124
|
+
recurrenceThreshold: min,
|
|
125
|
+
status: retained === 0 ? (skipped ? 'inconclusive' : 'no-evidence') : (skipped ? 'partial' : 'evidence'),
|
|
126
|
+
classes,
|
|
127
|
+
unclassified: { occurrences: unclassified.length, evidenceRefs: unclassifiedEvidence.refs, omittedEvidenceRefs: unclassifiedEvidence.omitted },
|
|
128
|
+
sources: { ...scan.sources, skippedByReason: { ...scan.sources.skippedByReason } },
|
|
129
|
+
omittedEvidenceRefs,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toClassStatus = toClassStatus;
|
|
4
|
+
exports.reduceClassStatus = reduceClassStatus;
|
|
5
|
+
exports.crossEmpiricalOutcome = crossEmpiricalOutcome;
|
|
3
6
|
exports.evaluateCoverage = evaluateCoverage;
|
|
4
7
|
const detectorStatuses = ['covered', 'missing', 'disabled', 'ineffective', 'unverifiable'];
|
|
5
8
|
const commandStatuses = ['matched', 'custom', 'missing'];
|
|
6
9
|
const fileStatuses = ['matched', 'missing', 'unverifiable'];
|
|
10
|
+
const compatibilityStates = [
|
|
11
|
+
'certified', 'compatible-unverified', 'incompatible', 'missing-tool', 'unverifiable', 'not-applicable',
|
|
12
|
+
];
|
|
7
13
|
function keyFor(classId, detectorIndex) {
|
|
8
14
|
return `${classId}:${detectorIndex}`;
|
|
9
15
|
}
|
|
@@ -31,6 +37,20 @@ function validateEvidence(evidence) {
|
|
|
31
37
|
return false;
|
|
32
38
|
});
|
|
33
39
|
}
|
|
40
|
+
function validateCompatibility(value) {
|
|
41
|
+
if (!isRecord(value) || !compatibilityStates.includes(value.state)
|
|
42
|
+
|| !isNonEmptyString(value.reason)
|
|
43
|
+
|| !(value.variantId === null || isNonEmptyString(value.variantId))
|
|
44
|
+
|| !(value.toolVersion === null || isNonEmptyString(value.toolVersion))
|
|
45
|
+
|| !(value.runtimeVersion === null || isNonEmptyString(value.runtimeVersion))
|
|
46
|
+
|| !(value.certifiedRange === null || isNonEmptyString(value.certifiedRange))
|
|
47
|
+
|| !Array.isArray(value.evidence))
|
|
48
|
+
return false;
|
|
49
|
+
return value.evidence.every((item) => isRecord(item)
|
|
50
|
+
&& isNonEmptyString(item.kind) && isNonEmptyString(item.status)
|
|
51
|
+
&& (!('path' in item) || isNonEmptyString(item.path))
|
|
52
|
+
&& Object.keys(item).every((key) => key === 'kind' || key === 'status' || key === 'path'));
|
|
53
|
+
}
|
|
34
54
|
function validateContract(contract) {
|
|
35
55
|
if (!isRecord(contract) || contract.schemaVersion !== 1 || !isRecord(contract.classes) || Object.keys(contract.classes).length === 0) {
|
|
36
56
|
throw new Error('evaluateCoverage: invalid coverage contract');
|
|
@@ -52,11 +72,64 @@ function validateContract(contract) {
|
|
|
52
72
|
function validateObservation(item) {
|
|
53
73
|
return isRecord(item) && isNonEmptyString(item.classId) && typeof item.detectorIndex === 'number' && Number.isSafeInteger(item.detectorIndex)
|
|
54
74
|
&& item.detectorIndex >= 0 && isNonEmptyString(item.sensor) && typeof item.status === 'string'
|
|
55
|
-
&& detectorStatuses.includes(item.status) && validateEvidence(item.evidence)
|
|
75
|
+
&& detectorStatuses.includes(item.status) && validateEvidence(item.evidence)
|
|
76
|
+
&& (!('compatibility' in item) || validateCompatibility(item.compatibility));
|
|
56
77
|
}
|
|
57
78
|
function copyEvidence(evidence) {
|
|
58
79
|
return evidence.map((item) => ({ ...item }));
|
|
59
80
|
}
|
|
81
|
+
function copyCompatibility(value) {
|
|
82
|
+
return value === undefined ? undefined : {
|
|
83
|
+
...value,
|
|
84
|
+
evidence: value.evidence.map((item) => ({ ...item })),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Map a detector's live compatibility plus its R2 structural evidence to a
|
|
88
|
+
* deterministic class alternative. A missing compatibility field is the R2
|
|
89
|
+
* compatibility path retained for direct callers while legacy/v2 coverage
|
|
90
|
+
* collection always supplies an explicit live resolution. */
|
|
91
|
+
function toClassStatus(detector) {
|
|
92
|
+
if (!detector || typeof detector !== 'object' || !detectorStatuses.includes(detector.status)
|
|
93
|
+
|| !validateEvidence(detector.evidence))
|
|
94
|
+
throw new Error('toClassStatus: malformed detector observation');
|
|
95
|
+
const state = detector.compatibility?.state;
|
|
96
|
+
if (state === 'not-applicable')
|
|
97
|
+
return 'not-applicable';
|
|
98
|
+
if (state === 'compatible-unverified' || state === 'unverifiable')
|
|
99
|
+
return 'unverifiable';
|
|
100
|
+
if (state === 'incompatible' || state === 'missing-tool')
|
|
101
|
+
return 'missing';
|
|
102
|
+
if (detector.status === 'unverifiable')
|
|
103
|
+
return 'unverifiable';
|
|
104
|
+
if (detector.status === 'covered')
|
|
105
|
+
return 'covered';
|
|
106
|
+
return 'missing';
|
|
107
|
+
}
|
|
108
|
+
const rank = {
|
|
109
|
+
covered: 4,
|
|
110
|
+
unverifiable: 3,
|
|
111
|
+
missing: 2,
|
|
112
|
+
'not-applicable': 1,
|
|
113
|
+
};
|
|
114
|
+
function reduceClassStatus(detectors) {
|
|
115
|
+
if (!Array.isArray(detectors) || detectors.length === 0)
|
|
116
|
+
throw new Error('coverage class requires detectors');
|
|
117
|
+
const statuses = detectors.map(toClassStatus);
|
|
118
|
+
return statuses.reduce((best, current) => rank[current] > rank[best] ? current : best);
|
|
119
|
+
}
|
|
120
|
+
/** Shared static/empirical vocabulary. The empirical analyzer supplies the
|
|
121
|
+
* occurrence boolean in T6; keeping the contradiction decision here prevents
|
|
122
|
+
* a second interpretation of applicability in that later layer. */
|
|
123
|
+
function crossEmpiricalOutcome(status, hasEvidence) {
|
|
124
|
+
if (typeof hasEvidence !== 'boolean' || !['covered', 'missing', 'unverifiable', 'not-applicable'].includes(status)) {
|
|
125
|
+
throw new Error('crossEmpiricalOutcome: invalid static status or evidence flag');
|
|
126
|
+
}
|
|
127
|
+
if (status === 'not-applicable' && hasEvidence)
|
|
128
|
+
return 'applicability-contradiction';
|
|
129
|
+
if (status === 'covered')
|
|
130
|
+
return 'covered-by-sensor';
|
|
131
|
+
return status === 'missing' ? 'gap' : 'coverage-unverifiable';
|
|
132
|
+
}
|
|
60
133
|
function evaluateCoverage(contract, observations) {
|
|
61
134
|
validateContract(contract);
|
|
62
135
|
if (!Array.isArray(observations))
|
|
@@ -80,11 +153,14 @@ function evaluateCoverage(contract, observations) {
|
|
|
80
153
|
if (found.sensor !== detector.sensor) {
|
|
81
154
|
throw new Error(`evaluateCoverage: observation for '${identity}' must use sensor '${detector.sensor}'`);
|
|
82
155
|
}
|
|
83
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
sensor: found.sensor,
|
|
158
|
+
status: found.status,
|
|
159
|
+
evidence: copyEvidence(found.evidence),
|
|
160
|
+
...(found.compatibility === undefined ? {} : { compatibility: copyCompatibility(found.compatibility) }),
|
|
161
|
+
};
|
|
84
162
|
});
|
|
85
|
-
const status = detectors
|
|
86
|
-
: detectors.some((item) => item.status === 'unverifiable') ? 'unverifiable'
|
|
87
|
-
: 'missing';
|
|
163
|
+
const status = reduceClassStatus(detectors);
|
|
88
164
|
return {
|
|
89
165
|
id,
|
|
90
166
|
description: expected.description,
|
|
@@ -100,8 +176,10 @@ function evaluateCoverage(contract, observations) {
|
|
|
100
176
|
throw new Error(`evaluateCoverage: unexpected observation for '${identity}' (${observation.sensor})`);
|
|
101
177
|
}
|
|
102
178
|
}
|
|
103
|
-
const
|
|
179
|
+
const applicable = classes.filter((item) => item.status !== 'not-applicable');
|
|
180
|
+
const overall = applicable.some((item) => item.status === 'missing') ? 'gaps'
|
|
104
181
|
: classes.some((item) => item.status === 'unverifiable') ? 'inconclusive'
|
|
105
|
-
: 'covered'
|
|
182
|
+
: applicable.some((item) => item.status === 'covered') ? 'covered'
|
|
183
|
+
: 'inconclusive';
|
|
106
184
|
return { overall, classes };
|
|
107
185
|
}
|
|
@@ -98,7 +98,11 @@ function observeDetector(root, classId, detectorIndex, detector, sensor, io = re
|
|
|
98
98
|
if (sensor.enabled === false)
|
|
99
99
|
return { ...base, status: 'disabled', evidence: [] };
|
|
100
100
|
const requiredCommand = detector.evidence?.commandIncludes ?? [];
|
|
101
|
-
|
|
101
|
+
// v2 commands are compared only in-memory to the catalog's structural
|
|
102
|
+
// fragments. Neither the command nor its args cross the coverage boundary.
|
|
103
|
+
const command = 'command' in sensor
|
|
104
|
+
? [sensor.command.executable, ...sensor.command.args].join(' ')
|
|
105
|
+
: sensor.cmd;
|
|
102
106
|
if (requiredCommand.length > 0 && typeof command !== 'string') {
|
|
103
107
|
return { ...base, status: 'unverifiable', evidence: [{ kind: 'command', status: 'missing' }] };
|
|
104
108
|
}
|
|
@@ -1,31 +1,68 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.runCoverage = runCoverage;
|
|
4
7
|
const evidence_1 = require("./evidence");
|
|
5
8
|
const evaluate_1 = require("./evaluate");
|
|
6
9
|
const resolve_1 = require("./resolve");
|
|
7
|
-
const
|
|
10
|
+
const live_1 = require("../compatibility/live");
|
|
11
|
+
const manifest_1 = require("../compatibility/manifest");
|
|
12
|
+
const scan_1 = require("../../../core/ledger/scan");
|
|
13
|
+
const empirical_1 = require("./empirical");
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const defaults = { resolve: resolve_1.resolveCoverageInputs, observe: evidence_1.observeDetector, resolveLive: live_1.resolveLiveCompatibility, scan: scan_1.scanProjectLedgers };
|
|
16
|
+
function normalizedMin(value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return 2;
|
|
19
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
|
|
20
|
+
throw new Error('runCoverage: min must be a positive safe integer');
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function empirical(deps, projectRoot, states, min, staticAvailability = 'available') {
|
|
24
|
+
return (0, empirical_1.evaluateEmpiricalCoverage)(deps.scan(projectRoot), states, min, staticAvailability);
|
|
25
|
+
}
|
|
8
26
|
/** Build static coverage data only: no command execution and no writes. */
|
|
9
|
-
function runCoverage(cwd, dependencies = {}) {
|
|
27
|
+
async function runCoverage(cwd, dependencies = {}, options = {}) {
|
|
10
28
|
if (typeof cwd !== 'string' || cwd.trim().length === 0)
|
|
11
29
|
throw new Error('runCoverage: cwd must be a non-empty string');
|
|
30
|
+
const min = normalizedMin(options.min);
|
|
12
31
|
const deps = { ...defaults, ...dependencies };
|
|
13
|
-
const input = deps.resolve(cwd);
|
|
32
|
+
const input = await deps.resolve(cwd);
|
|
14
33
|
if (input.kind === 'not_configured') {
|
|
15
|
-
return { schemaVersion:
|
|
34
|
+
return { schemaVersion: 2, pack: null, registry: null, overall: 'inconclusive', static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
|
|
35
|
+
empirical: empirical(deps, path_1.default.resolve(cwd), {}, min, 'unavailable') };
|
|
16
36
|
}
|
|
17
37
|
if (input.kind === 'no_reference') {
|
|
18
|
-
return { schemaVersion:
|
|
38
|
+
return { schemaVersion: 2, pack: input.pack, registry: input.registry, overall: 'inconclusive', static: { status: 'inconclusive', reason: 'no_reference', classes: [] },
|
|
39
|
+
empirical: empirical(deps, input.projectRoot, {}, min, 'unavailable') };
|
|
40
|
+
}
|
|
41
|
+
let live;
|
|
42
|
+
if (input.manifest.kind === 'v2') {
|
|
43
|
+
live = (await deps.resolveLive(input.projectRoot, input.pack, input.manifest.pack.registryRoot)).sensors;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
live = Object.fromEntries(Object.keys(input.manifest.pack.sensors).map((name) => [name, (0, manifest_1.legacyCompatibility)('legacy manifest without schemaVersion')]));
|
|
19
47
|
}
|
|
20
48
|
const observations = [];
|
|
21
49
|
for (const [classId, coverageClass] of Object.entries(input.contract.classes)) {
|
|
22
50
|
coverageClass.detectors.forEach((detector, detectorIndex) => {
|
|
23
|
-
|
|
51
|
+
const observed = deps.observe(input.projectRoot, classId, detectorIndex, detector, input.manifest.pack.sensors[detector.sensor]);
|
|
52
|
+
observations.push({
|
|
53
|
+
...observed,
|
|
54
|
+
compatibility: live[detector.sensor] ?? {
|
|
55
|
+
state: 'unverifiable', reason: 'sensor-not-resolved', variantId: null,
|
|
56
|
+
toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [],
|
|
57
|
+
},
|
|
58
|
+
});
|
|
24
59
|
});
|
|
25
60
|
}
|
|
26
61
|
const evaluated = (0, evaluate_1.evaluateCoverage)(input.contract, observations);
|
|
62
|
+
const states = Object.fromEntries(evaluated.classes.map((entry) => [entry.id, entry.status]));
|
|
27
63
|
return {
|
|
28
|
-
schemaVersion:
|
|
64
|
+
schemaVersion: 2, pack: input.pack, registry: input.registry, overall: evaluated.overall,
|
|
29
65
|
static: { status: evaluated.overall, reason: null, classes: evaluated.classes },
|
|
66
|
+
empirical: empirical(deps, input.projectRoot, states, min),
|
|
30
67
|
};
|
|
31
68
|
}
|