agentic-workflow-manager 6.5.2 → 8.1.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 +382 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +92 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +134 -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 +99 -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 +244 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +95 -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 +193 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -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 +140 -2
- package/dist/tests/commands/sensors/exec.test.js +115 -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 +204 -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
|
@@ -6,11 +6,17 @@ const ANSI = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
|
6
6
|
const OSC = /\x1B\][\s\S]*?(?:\x07|\x1B\\)/g;
|
|
7
7
|
const CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
8
8
|
const OVERALL = ['covered', 'gaps', 'inconclusive'];
|
|
9
|
-
const CLASS_STATUS = ['covered', 'missing', 'unverifiable'];
|
|
9
|
+
const CLASS_STATUS = ['covered', 'missing', 'unverifiable', 'not-applicable'];
|
|
10
10
|
const DETECTOR_STATUS = ['covered', 'missing', 'disabled', 'ineffective', 'unverifiable'];
|
|
11
|
+
const COMPATIBILITY_STATE = ['certified', 'compatible-unverified', 'incompatible', 'missing-tool', 'unverifiable', 'not-applicable'];
|
|
11
12
|
const REASON = ['not_configured', 'no_reference'];
|
|
12
13
|
const COMMAND_EVIDENCE_STATUS = ['matched', 'custom', 'missing'];
|
|
13
14
|
const FILE_EVIDENCE_STATUS = ['matched', 'missing', 'unverifiable'];
|
|
15
|
+
const EMPIRICAL_STATUS = ['no-evidence', 'evidence', 'partial', 'inconclusive'];
|
|
16
|
+
const EMPIRICAL_OUTCOME = ['covered-by-sensor', 'gap', 'coverage-unverifiable', 'applicability-contradiction', 'unmapped-class'];
|
|
17
|
+
const SEVERITY = ['blocker', 'important', 'minor', 'info'];
|
|
18
|
+
const CLUSTER_KIND = ['exact', 'convergent'];
|
|
19
|
+
const SAFE_REF = /^(?:PR #[1-9][0-9]*|[a-f0-9]{7,64}|(?!\/)(?!.*(?:^|\/)\.\.?\/)[A-Za-z0-9._@+~=-]+(?:\/[A-Za-z0-9._@+~=-]+)*:[1-9][0-9]*)$/i;
|
|
14
20
|
function isRecord(value) {
|
|
15
21
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
22
|
}
|
|
@@ -53,10 +59,149 @@ function assertEvidence(evidence, renderer) {
|
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
62
|
+
function assertCompatibility(value, renderer) {
|
|
63
|
+
if (!isRecord(value) || !hasExactFields(value, ['state', 'reason', 'variantId', 'toolVersion', 'runtimeVersion', 'certifiedRange', 'evidence'])
|
|
64
|
+
|| !isOneOf(value.state, COMPATIBILITY_STATE) || !isNonBlankString(value.reason)
|
|
65
|
+
|| !(value.variantId === null || isNonBlankString(value.variantId))
|
|
66
|
+
|| !(value.toolVersion === null || isNonBlankString(value.toolVersion))
|
|
67
|
+
|| !(value.runtimeVersion === null || isNonBlankString(value.runtimeVersion))
|
|
68
|
+
|| !(value.certifiedRange === null || isNonBlankString(value.certifiedRange))
|
|
69
|
+
|| !Array.isArray(value.evidence))
|
|
70
|
+
invalidReport(renderer);
|
|
71
|
+
for (const evidence of value.evidence) {
|
|
72
|
+
if (!isRecord(evidence) || !hasExactFields(evidence, 'path' in evidence ? ['kind', 'status', 'path'] : ['kind', 'status'])
|
|
73
|
+
|| !isNonBlankString(evidence.kind) || !isNonBlankString(evidence.status)
|
|
74
|
+
|| ('path' in evidence && !isNonBlankString(evidence.path)))
|
|
75
|
+
invalidReport(renderer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function assertCount(value, renderer) {
|
|
79
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
80
|
+
invalidReport(renderer);
|
|
81
|
+
}
|
|
82
|
+
function assertRefs(value, renderer) {
|
|
83
|
+
if (!Array.isArray(value) || value.some((ref) => !isNonBlankString(ref) || ref.length > 256 || /[\u0000-\u001F\u007F-\u009F]/.test(ref) || !SAFE_REF.test(ref)))
|
|
84
|
+
invalidReport(renderer);
|
|
85
|
+
for (let index = 1; index < value.length; index += 1)
|
|
86
|
+
if (value[index - 1] >= value[index])
|
|
87
|
+
invalidReport(renderer);
|
|
88
|
+
}
|
|
89
|
+
function assertClusters(value, threshold, renderer) {
|
|
90
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
91
|
+
invalidReport(renderer);
|
|
92
|
+
let occurrences = 0;
|
|
93
|
+
let recurrent = false;
|
|
94
|
+
let highestSeverity = 'info';
|
|
95
|
+
const severityRank = { blocker: 4, important: 3, minor: 2, info: 1 };
|
|
96
|
+
for (const cluster of value) {
|
|
97
|
+
if (!isRecord(cluster) || !hasExactFields(cluster, ['occurrences', 'recurrent', 'severity', 'kind', 'signatures', 'omittedSignatures', 'evidenceRefs', 'omittedEvidenceRefs'])
|
|
98
|
+
|| !isOneOf(cluster.kind, CLUSTER_KIND) || !isOneOf(cluster.severity, SEVERITY) || typeof cluster.recurrent !== 'boolean')
|
|
99
|
+
invalidReport(renderer);
|
|
100
|
+
assertCount(cluster.occurrences, renderer);
|
|
101
|
+
assertRefs(cluster.signatures, renderer);
|
|
102
|
+
assertCount(cluster.omittedSignatures, renderer);
|
|
103
|
+
assertRefs(cluster.evidenceRefs, renderer);
|
|
104
|
+
assertCount(cluster.omittedEvidenceRefs, renderer);
|
|
105
|
+
if (cluster.occurrences < 1 || cluster.recurrent !== (cluster.occurrences >= threshold))
|
|
106
|
+
invalidReport(renderer);
|
|
107
|
+
occurrences += cluster.occurrences;
|
|
108
|
+
recurrent ||= cluster.recurrent;
|
|
109
|
+
if (severityRank[cluster.severity] > severityRank[highestSeverity])
|
|
110
|
+
highestSeverity = cluster.severity;
|
|
111
|
+
}
|
|
112
|
+
return { occurrences, recurrent, severity: highestSeverity };
|
|
113
|
+
}
|
|
114
|
+
function assertEmpirical(value, renderer) {
|
|
115
|
+
if (!isRecord(value) || !hasExactFields(value, ['recurrenceThreshold', 'status', 'classes', 'unclassified', 'sources', 'omittedEvidenceRefs'])
|
|
116
|
+
|| !isOneOf(value.status, EMPIRICAL_STATUS) || !Array.isArray(value.classes) || !isRecord(value.unclassified)
|
|
117
|
+
|| !isRecord(value.sources))
|
|
118
|
+
invalidReport(renderer);
|
|
119
|
+
assertCount(value.omittedEvidenceRefs, renderer);
|
|
120
|
+
if (typeof value.recurrenceThreshold !== 'number' || !Number.isSafeInteger(value.recurrenceThreshold) || value.recurrenceThreshold < 1)
|
|
121
|
+
invalidReport(renderer);
|
|
122
|
+
let previous;
|
|
123
|
+
let occurrenceCount = 0;
|
|
124
|
+
for (const item of value.classes) {
|
|
125
|
+
if (!isRecord(item) || !hasExactFields(item, ['defectClass', 'occurrences', 'recurrent', 'severity', 'outcome', 'evidenceRefs', 'omittedEvidenceRefs', 'clusters'])
|
|
126
|
+
|| !isNonBlankString(item.defectClass) || !isOneOf(item.severity, SEVERITY) || !isOneOf(item.outcome, EMPIRICAL_OUTCOME)
|
|
127
|
+
|| typeof item.recurrent !== 'boolean')
|
|
128
|
+
invalidReport(renderer);
|
|
129
|
+
assertCount(item.occurrences, renderer);
|
|
130
|
+
assertCount(item.omittedEvidenceRefs, renderer);
|
|
131
|
+
assertRefs(item.evidenceRefs, renderer);
|
|
132
|
+
const clusterSummary = assertClusters(item.clusters, value.recurrenceThreshold, renderer);
|
|
133
|
+
if (item.occurrences < 1 || item.occurrences !== clusterSummary.occurrences || item.recurrent !== clusterSummary.recurrent
|
|
134
|
+
|| item.severity !== clusterSummary.severity)
|
|
135
|
+
invalidReport(renderer);
|
|
136
|
+
if (previous && (Number(previous.recurrent) < Number(item.recurrent)
|
|
137
|
+
|| (previous.recurrent === item.recurrent && previous.occurrences < item.occurrences)
|
|
138
|
+
|| (previous.recurrent === item.recurrent && previous.occurrences === item.occurrences && previous.defectClass >= item.defectClass)))
|
|
139
|
+
invalidReport(renderer);
|
|
140
|
+
previous = { recurrent: item.recurrent, occurrences: item.occurrences, defectClass: item.defectClass };
|
|
141
|
+
occurrenceCount += item.occurrences;
|
|
142
|
+
}
|
|
143
|
+
if (!hasExactFields(value.unclassified, ['occurrences', 'evidenceRefs', 'omittedEvidenceRefs']))
|
|
144
|
+
invalidReport(renderer);
|
|
145
|
+
assertCount(value.unclassified.occurrences, renderer);
|
|
146
|
+
assertCount(value.unclassified.omittedEvidenceRefs, renderer);
|
|
147
|
+
assertRefs(value.unclassified.evidenceRefs, renderer);
|
|
148
|
+
if (!hasExactFields(value.sources, ['activeFiles', 'archivedFiles', 'validEntries', 'validFindings', 'skippedFindings', 'skippedByReason']) || !isRecord(value.sources.skippedByReason))
|
|
149
|
+
invalidReport(renderer);
|
|
150
|
+
for (const key of ['activeFiles', 'archivedFiles', 'validEntries', 'validFindings', 'skippedFindings'])
|
|
151
|
+
assertCount(value.sources[key], renderer);
|
|
152
|
+
for (const count of Object.values(value.sources.skippedByReason))
|
|
153
|
+
assertCount(count, renderer);
|
|
154
|
+
const validFindings = value.sources.validFindings;
|
|
155
|
+
const validEntries = value.sources.validEntries;
|
|
156
|
+
const skippedFindings = value.sources.skippedFindings;
|
|
157
|
+
const unclassifiedOccurrences = value.unclassified.occurrences;
|
|
158
|
+
const omitted = value.omittedEvidenceRefs;
|
|
159
|
+
if (validFindings !== occurrenceCount + unclassifiedOccurrences || validEntries < validFindings)
|
|
160
|
+
invalidReport(renderer);
|
|
161
|
+
const skippedByReason = Object.values(value.sources.skippedByReason);
|
|
162
|
+
const totalSkippedByReason = skippedByReason.reduce((total, count) => total + count, 0);
|
|
163
|
+
if (skippedFindings !== totalSkippedByReason)
|
|
164
|
+
invalidReport(renderer);
|
|
165
|
+
const classOmitted = value.classes.reduce((total, item) => total + item.omittedEvidenceRefs, 0);
|
|
166
|
+
const unclassifiedOmitted = value.unclassified.omittedEvidenceRefs;
|
|
167
|
+
const scannerOmitted = (value.sources.skippedByReason['evidence-ref-limit'] ?? 0);
|
|
168
|
+
if (omitted !== scannerOmitted + classOmitted + unclassifiedOmitted)
|
|
169
|
+
invalidReport(renderer);
|
|
170
|
+
const incomplete = skippedFindings + omitted + unclassifiedOccurrences;
|
|
171
|
+
const expectedStatus = validFindings === 0
|
|
172
|
+
? incomplete === 0 ? 'no-evidence' : 'inconclusive'
|
|
173
|
+
: incomplete === 0 ? 'evidence' : 'partial';
|
|
174
|
+
if (value.status !== expectedStatus)
|
|
175
|
+
invalidReport(renderer);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* An empirical class is public only as an aggregate, but its outcome is still
|
|
179
|
+
* constrained by the already-public static catalog. Keeping the lookup local
|
|
180
|
+
* to the renderer avoids expanding the JSON schema with an internal state map.
|
|
181
|
+
*/
|
|
182
|
+
function assertEmpiricalStaticInvariant(empirical, staticStates, renderer) {
|
|
183
|
+
for (const item of empirical.classes) {
|
|
184
|
+
let expected;
|
|
185
|
+
if (staticStates === null) {
|
|
186
|
+
expected = 'coverage-unverifiable';
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const state = staticStates.get(item.defectClass);
|
|
190
|
+
expected = state === undefined ? 'unmapped-class'
|
|
191
|
+
: state === 'covered' ? 'covered-by-sensor'
|
|
192
|
+
: state === 'missing' ? 'gap'
|
|
193
|
+
: state === 'unverifiable' ? 'coverage-unverifiable'
|
|
194
|
+
: state === 'not-applicable' ? 'applicability-contradiction'
|
|
195
|
+
: invalidReport(renderer);
|
|
196
|
+
}
|
|
197
|
+
if (item.outcome !== expected)
|
|
198
|
+
invalidReport(renderer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
56
201
|
function assertCoverageEnvelope(report, renderer) {
|
|
57
202
|
if (!isRecord(report) || !hasExactFields(report, 'empirical' in report
|
|
58
203
|
? ['schemaVersion', 'pack', 'registry', 'overall', 'static', 'empirical']
|
|
59
|
-
: ['schemaVersion', 'pack', 'registry', 'overall', 'static']) || report.schemaVersion !==
|
|
204
|
+
: ['schemaVersion', 'pack', 'registry', 'overall', 'static']) || report.schemaVersion !== 2
|
|
60
205
|
|| !(report.pack === null || isNonBlankString(report.pack))
|
|
61
206
|
|| !(report.registry === null || isNonBlankString(report.registry))
|
|
62
207
|
|| !isOneOf(report.overall, OVERALL) || !isRecord(report.static)) {
|
|
@@ -73,6 +218,10 @@ function assertCoverageEnvelope(report, renderer) {
|
|
|
73
218
|
if (report.overall !== 'inconclusive' || report.pack !== null || report.registry !== null || staticReport.classes.length !== 0) {
|
|
74
219
|
invalidReport(renderer);
|
|
75
220
|
}
|
|
221
|
+
if (report.empirical !== undefined) {
|
|
222
|
+
assertEmpirical(report.empirical, renderer);
|
|
223
|
+
assertEmpiricalStaticInvariant(report.empirical, null, renderer);
|
|
224
|
+
}
|
|
76
225
|
return;
|
|
77
226
|
}
|
|
78
227
|
if (staticReport.reason === 'no_reference') {
|
|
@@ -80,6 +229,10 @@ function assertCoverageEnvelope(report, renderer) {
|
|
|
80
229
|
|| staticReport.classes.length !== 0) {
|
|
81
230
|
invalidReport(renderer);
|
|
82
231
|
}
|
|
232
|
+
if (report.empirical !== undefined) {
|
|
233
|
+
assertEmpirical(report.empirical, renderer);
|
|
234
|
+
assertEmpiricalStaticInvariant(report.empirical, null, renderer);
|
|
235
|
+
}
|
|
83
236
|
return;
|
|
84
237
|
}
|
|
85
238
|
if (!isNonBlankString(report.pack) || !isNonBlankString(report.registry) || staticReport.classes.length === 0) {
|
|
@@ -88,6 +241,7 @@ function assertCoverageEnvelope(report, renderer) {
|
|
|
88
241
|
let previousId;
|
|
89
242
|
let hasMissingClass = false;
|
|
90
243
|
let hasUnverifiableClass = false;
|
|
244
|
+
const staticStates = new Map();
|
|
91
245
|
for (const coverageClass of staticReport.classes) {
|
|
92
246
|
if (!isRecord(coverageClass) || !hasExactFields(coverageClass, ['id', 'description', 'status', 'detectors', 'remedy'])
|
|
93
247
|
|| !isNonBlankString(coverageClass.id) || !isNonBlankString(coverageClass.description)
|
|
@@ -102,26 +256,39 @@ function assertCoverageEnvelope(report, renderer) {
|
|
|
102
256
|
invalidReport(renderer);
|
|
103
257
|
if (!isNonBlankString(coverageClass.remedy.summary) || !isNonBlankString(coverageClass.remedy.command))
|
|
104
258
|
invalidReport(renderer);
|
|
105
|
-
let
|
|
106
|
-
let hasUnverifiableDetector = false;
|
|
259
|
+
let classStatus;
|
|
107
260
|
for (const detector of coverageClass.detectors) {
|
|
108
|
-
if (!isRecord(detector) || !hasExactFields(detector, ['sensor', 'status', 'evidence'])
|
|
261
|
+
if (!isRecord(detector) || !hasExactFields(detector, ['sensor', 'status', 'evidence', 'compatibility'])
|
|
109
262
|
|| !isNonBlankString(detector.sensor) || !isOneOf(detector.status, DETECTOR_STATUS)) {
|
|
110
263
|
invalidReport(renderer);
|
|
111
264
|
}
|
|
112
265
|
assertEvidence(detector.evidence, renderer);
|
|
113
|
-
|
|
114
|
-
|
|
266
|
+
assertCompatibility(detector.compatibility, renderer);
|
|
267
|
+
const state = detector.compatibility.state;
|
|
268
|
+
const detectorStatus = state === 'not-applicable' ? 'not-applicable'
|
|
269
|
+
: state === 'compatible-unverified' || state === 'unverifiable' || detector.status === 'unverifiable' ? 'unverifiable'
|
|
270
|
+
: state === 'incompatible' || state === 'missing-tool' || detector.status !== 'covered' ? 'missing'
|
|
271
|
+
: 'covered';
|
|
272
|
+
const rank = { covered: 4, unverifiable: 3, missing: 2, 'not-applicable': 1 };
|
|
273
|
+
if (classStatus === undefined || rank[detectorStatus] > rank[classStatus])
|
|
274
|
+
classStatus = detectorStatus;
|
|
115
275
|
}
|
|
116
|
-
const expectedClassStatus =
|
|
276
|
+
const expectedClassStatus = classStatus;
|
|
117
277
|
if (coverageClass.status !== expectedClassStatus)
|
|
118
278
|
invalidReport(renderer);
|
|
279
|
+
staticStates.set(coverageClass.id, expectedClassStatus);
|
|
119
280
|
hasMissingClass ||= expectedClassStatus === 'missing';
|
|
120
281
|
hasUnverifiableClass ||= expectedClassStatus === 'unverifiable';
|
|
121
282
|
}
|
|
122
|
-
const
|
|
283
|
+
const applicableClasses = staticReport.classes.filter((entry) => entry.status !== 'not-applicable');
|
|
284
|
+
const expectedOverall = hasMissingClass ? 'gaps' : hasUnverifiableClass ? 'inconclusive'
|
|
285
|
+
: applicableClasses.some((entry) => entry.status === 'covered') ? 'covered' : 'inconclusive';
|
|
123
286
|
if (report.overall !== expectedOverall)
|
|
124
287
|
invalidReport(renderer);
|
|
288
|
+
if (report.empirical !== undefined) {
|
|
289
|
+
assertEmpirical(report.empirical, renderer);
|
|
290
|
+
assertEmpiricalStaticInvariant(report.empirical, staticStates, renderer);
|
|
291
|
+
}
|
|
125
292
|
}
|
|
126
293
|
function safeHumanText(value) {
|
|
127
294
|
return value.replace(OSC, '').replace(ANSI, '').replace(CONTROLS, ' ');
|
|
@@ -133,20 +300,53 @@ function renderCoverageJson(report) {
|
|
|
133
300
|
function renderCoverageHuman(report) {
|
|
134
301
|
assertCoverageEnvelope(report, 'renderCoverageHuman');
|
|
135
302
|
if (report.static.reason === 'not_configured') {
|
|
136
|
-
return ['Sensor coverage', 'Overall: inconclusive', 'Reason: sensors are not configured', 'Run: awm sensors init', ''].join('\n');
|
|
303
|
+
return ['Sensor coverage', 'Overall: inconclusive', 'Reason: sensors are not configured', 'Run: awm sensors init', ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
137
304
|
}
|
|
138
305
|
if (report.static.reason === 'no_reference') {
|
|
139
306
|
return ['Sensor coverage', `Pack: ${safeHumanText(report.pack ?? 'unknown')}`, `Registry: ${safeHumanText(report.registry ?? 'unknown')}`,
|
|
140
|
-
'Overall: inconclusive', `No coverage reference for pack '${safeHumanText(report.pack ?? 'unknown')}'`, ''].join('\n');
|
|
307
|
+
'Overall: inconclusive', `No coverage reference for pack '${safeHumanText(report.pack ?? 'unknown')}'`, ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
141
308
|
}
|
|
142
309
|
const lines = ['Sensor coverage', `Pack: ${safeHumanText(report.pack ?? 'unknown')}`, `Registry: ${safeHumanText(report.registry ?? 'unknown')}`,
|
|
143
310
|
`Overall: ${report.overall}`, ''];
|
|
144
|
-
for (const item of report.static.classes.filter((entry) => entry.status !== 'covered')) {
|
|
311
|
+
for (const item of report.static.classes.filter((entry) => entry.status !== 'covered' && entry.status !== 'not-applicable')) {
|
|
145
312
|
lines.push(`${item.status} ${safeHumanText(item.id)} — ${safeHumanText(item.description)}`);
|
|
146
|
-
item.detectors.forEach((detector) =>
|
|
313
|
+
item.detectors.forEach((detector) => {
|
|
314
|
+
const compatibility = detector.compatibility;
|
|
315
|
+
if (!compatibility)
|
|
316
|
+
invalidReport('renderCoverageHuman');
|
|
317
|
+
lines.push(` detector: ${safeHumanText(detector.sensor)} (${detector.status})`, ` compatibility: ${safeHumanText(compatibility.state)} — ${safeHumanText(compatibility.reason)}`);
|
|
318
|
+
});
|
|
147
319
|
lines.push(` remedy: ${safeHumanText(item.remedy.summary)}`, ` command: ${safeHumanText(item.remedy.command)}`);
|
|
148
320
|
}
|
|
149
321
|
const count = (status) => report.static.classes.filter((item) => item.status === status).length;
|
|
150
|
-
lines.push('', `Summary: ${count('covered')} covered, ${count('missing')} missing, ${count('unverifiable')} unverifiable
|
|
322
|
+
lines.push('', `Summary: ${count('covered')} covered, ${count('missing')} missing, ${count('unverifiable')} unverifiable, ${count('not-applicable')} not applicable`);
|
|
323
|
+
if (report.empirical)
|
|
324
|
+
lines.push(empiricalHuman(report));
|
|
325
|
+
lines.push('');
|
|
326
|
+
return lines.join('\n');
|
|
327
|
+
}
|
|
328
|
+
function empiricalHuman(report) {
|
|
329
|
+
const empirical = report.empirical;
|
|
330
|
+
if (!empirical)
|
|
331
|
+
return '';
|
|
332
|
+
const lines = [`Empirical coverage: ${empirical.status}`];
|
|
333
|
+
for (const item of empirical.classes) {
|
|
334
|
+
const recurrence = item.recurrent
|
|
335
|
+
? `recurrent at threshold ${empirical.recurrenceThreshold}`
|
|
336
|
+
: `below recurrence threshold (${empirical.recurrenceThreshold})`;
|
|
337
|
+
lines.push(`${item.outcome} ${safeHumanText(item.defectClass)} — ${item.occurrences} occurrence${item.occurrences === 1 ? '' : 's'} (${item.severity}; ${recurrence})`);
|
|
338
|
+
if (item.evidenceRefs.length > 0)
|
|
339
|
+
lines.push(` evidence: ${item.evidenceRefs.map(safeHumanText).join(', ')}`);
|
|
340
|
+
for (const cluster of item.clusters) {
|
|
341
|
+
const detail = cluster.signatures.length > 0 ? ` — ${cluster.signatures.map(safeHumanText).join(', ')}` : '';
|
|
342
|
+
lines.push(` cluster: ${cluster.kind} (${cluster.occurrences} occurrence${cluster.occurrences === 1 ? '' : 's'}; ${cluster.severity})${detail}`);
|
|
343
|
+
if (cluster.omittedSignatures > 0)
|
|
344
|
+
lines.push(` omitted cluster signatures: ${cluster.omittedSignatures}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (empirical.unclassified.occurrences > 0)
|
|
348
|
+
lines.push(`unclassified findings: ${empirical.unclassified.occurrences}`);
|
|
349
|
+
if (empirical.omittedEvidenceRefs > 0)
|
|
350
|
+
lines.push(`omitted evidence refs: ${empirical.omittedEvidenceRefs}`);
|
|
151
351
|
return lines.join('\n');
|
|
152
352
|
}
|
|
@@ -7,8 +7,10 @@ exports.readBoundedJson = readBoundedJson;
|
|
|
7
7
|
exports.resolveCoverageInputs = resolveCoverageInputs;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const registries_1 = require("../../../core/registries");
|
|
11
10
|
const contract_1 = require("./contract");
|
|
11
|
+
const contract_2 = require("../compatibility/contract");
|
|
12
|
+
const manifest_1 = require("../compatibility/manifest");
|
|
13
|
+
const pack_source_1 = require("../compatibility/pack-source");
|
|
12
14
|
function readFailure(file, error) {
|
|
13
15
|
return new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
14
16
|
}
|
|
@@ -59,23 +61,6 @@ function readBoundedJson(file) {
|
|
|
59
61
|
throw new Error(`Invalid JSON at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
function readPackEnvelope(input, file, expectedName) {
|
|
63
|
-
if (typeof input !== 'object' || input === null || Array.isArray(input))
|
|
64
|
-
throw new Error(`Invalid pack at ${file}: expected object`);
|
|
65
|
-
const pack = input;
|
|
66
|
-
if (typeof pack.name !== 'string' || pack.name !== expectedName) {
|
|
67
|
-
throw new Error(`Invalid pack at ${file}: name must equal '${expectedName}'`);
|
|
68
|
-
}
|
|
69
|
-
if (typeof pack.sensors !== 'object' || pack.sensors === null || Array.isArray(pack.sensors)) {
|
|
70
|
-
throw new Error(`Invalid pack at ${file}: sensors must be an object`);
|
|
71
|
-
}
|
|
72
|
-
return 'coverage' in pack ? { coverage: pack.coverage } : {};
|
|
73
|
-
}
|
|
74
|
-
function safeRegistryName(name) {
|
|
75
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes('..')) {
|
|
76
|
-
throw new Error(`Invalid registry name '${name}': expected a safe path component`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
64
|
/** Finds the nearest manifest without following a symlink during discovery. */
|
|
80
65
|
function findManifestDirNoFollow(startCwd) {
|
|
81
66
|
let dir = path_1.default.resolve(startCwd);
|
|
@@ -102,25 +87,26 @@ function resolveCoverageInputs(cwd) {
|
|
|
102
87
|
if (!projectRoot)
|
|
103
88
|
return { kind: 'not_configured' };
|
|
104
89
|
const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
105
|
-
const manifest = (0,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return { kind: 'no_reference', projectRoot, pack: manifest.pack, registry: registry.name, manifest };
|
|
120
|
-
return {
|
|
121
|
-
kind: 'ready', projectRoot, pack: manifest.pack, registry: registry.name,
|
|
122
|
-
manifest, contract: (0, contract_1.parseCoverageContract)(coverage, packPath),
|
|
123
|
-
};
|
|
90
|
+
const manifest = (0, manifest_1.parseSensorManifest)(readBoundedJson(manifestPath), manifestPath);
|
|
91
|
+
const source = manifest.kind === 'v2' && manifest.pack.registryRoot !== undefined
|
|
92
|
+
? (0, pack_source_1.resolvePackSource)(manifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: manifest.pack.registryRoot }] })
|
|
93
|
+
: (0, pack_source_1.resolvePackSource)(manifest.pack.pack);
|
|
94
|
+
let sourceJson;
|
|
95
|
+
try {
|
|
96
|
+
sourceJson = JSON.parse(source.content);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`Invalid JSON at ${source.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
const parsedPack = (0, contract_2.parseSensorPack)(sourceJson, source.path);
|
|
102
|
+
if (parsedPack.pack.name !== manifest.pack.pack) {
|
|
103
|
+
throw new Error(`Invalid pack at ${source.path}: name must equal '${manifest.pack.pack}'`);
|
|
124
104
|
}
|
|
125
|
-
|
|
105
|
+
const { coverage } = parsedPack.pack;
|
|
106
|
+
if (coverage === undefined)
|
|
107
|
+
return { kind: 'no_reference', projectRoot, pack: manifest.pack.pack, registry: source.registry.name, manifest };
|
|
108
|
+
return {
|
|
109
|
+
kind: 'ready', projectRoot, pack: manifest.pack.pack, registry: source.registry.name,
|
|
110
|
+
manifest, contract: (0, contract_1.parseCoverageContract)(coverage, source.path),
|
|
111
|
+
};
|
|
126
112
|
}
|