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.
Files changed (95) hide show
  1. package/dist/scripts/sensor-support-matrix.js +103 -0
  2. package/dist/scripts/support-matrix.js +40 -17
  3. package/dist/src/commands/init.js +1 -3
  4. package/dist/src/commands/ledger/index.js +6 -0
  5. package/dist/src/commands/preflight/checks.js +4 -4
  6. package/dist/src/commands/preflight/index.js +2 -2
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +5 -0
  9. package/dist/src/commands/registry/status.js +7 -0
  10. package/dist/src/commands/sensors/compatibility/contract.js +382 -0
  11. package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
  12. package/dist/src/commands/sensors/compatibility/live.js +92 -0
  13. package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
  14. package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
  15. package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
  16. package/dist/src/commands/sensors/compatibility/probe.js +47 -0
  17. package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
  18. package/dist/src/commands/sensors/compatibility/types.js +2 -0
  19. package/dist/src/commands/sensors/coverage/contract.js +0 -50
  20. package/dist/src/commands/sensors/coverage/empirical.js +131 -0
  21. package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
  22. package/dist/src/commands/sensors/coverage/evidence.js +5 -1
  23. package/dist/src/commands/sensors/coverage/index.js +44 -7
  24. package/dist/src/commands/sensors/coverage/render.js +214 -14
  25. package/dist/src/commands/sensors/coverage/resolve.js +24 -38
  26. package/dist/src/commands/sensors/exec.js +244 -9
  27. package/dist/src/commands/sensors/index.js +18 -6
  28. package/dist/src/commands/sensors/init.js +95 -6
  29. package/dist/src/commands/sensors/run.js +71 -0
  30. package/dist/src/commands/sensors/status.js +48 -4
  31. package/dist/src/commands/sync.js +7 -0
  32. package/dist/src/core/bundles.js +38 -3
  33. package/dist/src/core/init/steps.js +7 -4
  34. package/dist/src/core/ledger/scan.js +228 -0
  35. package/dist/src/core/ledger/store.js +89 -25
  36. package/dist/src/core/ledger/types.js +54 -0
  37. package/dist/src/core/paths.js +10 -9
  38. package/dist/src/core/registries.js +134 -14
  39. package/dist/src/index.js +24 -1
  40. package/dist/tests/commands/ledger/index.test.js +11 -0
  41. package/dist/tests/commands/preflight/preflight.test.js +64 -64
  42. package/dist/tests/commands/registry/add.test.js +69 -2
  43. package/dist/tests/commands/registry/status.test.js +14 -0
  44. package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
  45. package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
  46. package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
  47. package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
  48. package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
  49. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
  50. package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
  51. package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
  52. package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
  53. package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
  54. package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
  55. package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
  56. package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
  57. package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
  58. package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
  59. package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
  60. package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
  61. package/dist/tests/commands/sensors/exec.test.js +115 -5
  62. package/dist/tests/commands/sensors/index.test.js +8 -0
  63. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
  64. package/dist/tests/commands/sensors/init.test.js +204 -48
  65. package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
  66. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  67. package/dist/tests/commands/sensors/run.test.js +66 -3
  68. package/dist/tests/commands/sensors/status-windows.test.js +7 -7
  69. package/dist/tests/commands/sensors/status.test.js +74 -23
  70. package/dist/tests/commands/update.test.js +1 -1
  71. package/dist/tests/core/bundles.test.js +37 -0
  72. package/dist/tests/core/discovery.test.js +4 -0
  73. package/dist/tests/core/init/orchestrator.test.js +5 -1
  74. package/dist/tests/core/init/steps.test.js +20 -4
  75. package/dist/tests/core/ledger/scan.test.js +217 -0
  76. package/dist/tests/core/ledger/store.test.js +108 -0
  77. package/dist/tests/core/profile-pins.test.js +2 -1
  78. package/dist/tests/core/project-skill-links.test.js +22 -0
  79. package/dist/tests/core/registries-capability.test.js +10 -0
  80. package/dist/tests/core/registries-sync.test.js +16 -0
  81. package/dist/tests/core/registries.test.js +129 -1
  82. package/dist/tests/core/registry-manifest.test.js +7 -0
  83. package/dist/tests/core/same-existing-path.test.js +35 -0
  84. package/dist/tests/core/sync-gates.test.js +2 -1
  85. package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
  86. package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
  87. package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
  88. package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
  89. package/dist/tests/structural/active-documentation.test.js +128 -0
  90. package/dist/tests/structural/async-entrypoint.test.js +15 -0
  91. package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
  92. package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
  93. package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
  94. package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
  95. 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 !== 1
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 hasCoveredDetector = false;
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
- hasCoveredDetector ||= detector.status === 'covered';
114
- hasUnverifiableDetector ||= detector.status === 'unverifiable';
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 = hasCoveredDetector ? 'covered' : hasUnverifiableDetector ? 'unverifiable' : 'missing';
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 expectedOverall = hasMissingClass ? 'gaps' : hasUnverifiableClass ? 'inconclusive' : 'covered';
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) => lines.push(` detector: ${safeHumanText(detector.sensor)} (${detector.status})`));
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, contract_1.parseCoverageManifest)(readBoundedJson(manifestPath), manifestPath);
106
- for (const registry of (0, registries_1.listRegistries)()) {
107
- safeRegistryName(registry.name);
108
- const packPath = path_1.default.join(registry.contentRoot, 'sensor-packs', manifest.pack, 'pack.json');
109
- try {
110
- fs_1.default.lstatSync(packPath);
111
- }
112
- catch (error) {
113
- if (error.code === 'ENOENT')
114
- continue;
115
- throw readFailure(packPath, error);
116
- }
117
- const { coverage } = readPackEnvelope(readBoundedJson(packPath), packPath, manifest.pack);
118
- if (coverage === undefined)
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
- throw new Error(`Pack '${manifest.pack}' was not found in configured registries`);
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
  }