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.
Files changed (93) 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 +261 -0
  11. package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
  12. package/dist/src/commands/sensors/compatibility/live.js +59 -0
  13. package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
  14. package/dist/src/commands/sensors/compatibility/materialize.js +126 -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 +96 -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 +150 -9
  27. package/dist/src/commands/sensors/index.js +18 -6
  28. package/dist/src/commands/sensors/init.js +92 -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 +98 -0
  45. package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
  46. package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
  47. package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
  48. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
  49. package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
  50. package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
  51. package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
  52. package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
  53. package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
  54. package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
  55. package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
  56. package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
  57. package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
  58. package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
  59. package/dist/tests/commands/sensors/exec.test.js +67 -5
  60. package/dist/tests/commands/sensors/index.test.js +8 -0
  61. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
  62. package/dist/tests/commands/sensors/init.test.js +132 -48
  63. package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
  64. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  65. package/dist/tests/commands/sensors/run.test.js +66 -3
  66. package/dist/tests/commands/sensors/status-windows.test.js +7 -7
  67. package/dist/tests/commands/sensors/status.test.js +74 -23
  68. package/dist/tests/commands/update.test.js +1 -1
  69. package/dist/tests/core/bundles.test.js +37 -0
  70. package/dist/tests/core/discovery.test.js +4 -0
  71. package/dist/tests/core/init/orchestrator.test.js +5 -1
  72. package/dist/tests/core/init/steps.test.js +20 -4
  73. package/dist/tests/core/ledger/scan.test.js +217 -0
  74. package/dist/tests/core/ledger/store.test.js +108 -0
  75. package/dist/tests/core/profile-pins.test.js +2 -1
  76. package/dist/tests/core/project-skill-links.test.js +22 -0
  77. package/dist/tests/core/registries-capability.test.js +10 -0
  78. package/dist/tests/core/registries-sync.test.js +16 -0
  79. package/dist/tests/core/registries.test.js +129 -1
  80. package/dist/tests/core/registry-manifest.test.js +7 -0
  81. package/dist/tests/core/same-existing-path.test.js +35 -0
  82. package/dist/tests/core/sync-gates.test.js +2 -1
  83. package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
  84. package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
  85. package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
  86. package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
  87. package/dist/tests/structural/active-documentation.test.js +128 -0
  88. package/dist/tests/structural/async-entrypoint.test.js +15 -0
  89. package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
  90. package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
  91. package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
  92. package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
  93. 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
  }
@@ -1,7 +1,13 @@
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 });
6
+ exports.runStructuredCommand = runStructuredCommand;
3
7
  exports.runCommand = runCommand;
4
8
  const child_process_1 = require("child_process");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
5
11
  const paths_1 = require("../../core/paths");
6
12
  const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
7
13
  const DEFAULT_KILL_GRACE_MS = 2_000;
@@ -53,7 +59,17 @@ function killTree(pid, signal) {
53
59
  * throws away 60s of eslint output costs double: the wall clock, and then
54
60
  * the re-run the caller has to do to learn anything at all.
55
61
  */
56
- function runCommand(cmd, opts) {
62
+ function validateOptions(opts) {
63
+ if (!opts || typeof opts !== 'object' || typeof opts.cwd !== 'string' || opts.cwd.trim() === '' || !Number.isSafeInteger(opts.timeout) || opts.timeout <= 0) {
64
+ throw new Error('exec options require a non-empty cwd and positive safe-integer timeout');
65
+ }
66
+ if (opts.maxBuffer !== undefined && (!Number.isSafeInteger(opts.maxBuffer) || opts.maxBuffer <= 0))
67
+ throw new Error('exec options maxBuffer must be a positive safe integer');
68
+ if (opts.killGraceMs !== undefined && (!Number.isSafeInteger(opts.killGraceMs) || opts.killGraceMs < 0))
69
+ throw new Error('exec options killGraceMs must be a non-negative safe integer');
70
+ }
71
+ function collectSpawn(input, opts) {
72
+ validateOptions(opts);
57
73
  const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
58
74
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
59
75
  return new Promise((resolve) => {
@@ -63,14 +79,21 @@ function runCommand(cmd, opts) {
63
79
  let overflowed = false;
64
80
  let settled = false;
65
81
  const timers = [];
66
- const child = (0, child_process_1.spawn)(cmd, {
67
- shell: true,
68
- cwd: opts.cwd,
69
- detached: !(0, paths_1.isWindowsNative)(),
70
- // stdin closed: a sensor must never block waiting for input, and the
71
- // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
72
- stdio: ['ignore', 'pipe', 'pipe'],
73
- });
82
+ const child = input.shell
83
+ ? (0, child_process_1.spawn)(input.executable, {
84
+ shell: true,
85
+ cwd: opts.cwd,
86
+ detached: !(0, paths_1.isWindowsNative)(),
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ })
89
+ : (0, child_process_1.spawn)(input.executable, input.args, {
90
+ shell: false,
91
+ cwd: opts.cwd,
92
+ detached: !(0, paths_1.isWindowsNative)(),
93
+ // stdin closed: a sensor must never block waiting for input, and the
94
+ // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
95
+ stdio: ['ignore', 'pipe', 'pipe'],
96
+ });
74
97
  const later = (fn, ms) => {
75
98
  const t = setTimeout(fn, ms);
76
99
  t.unref?.();
@@ -120,3 +143,121 @@ function runCommand(cmd, opts) {
120
143
  later(() => { timedOut = true; cutShort(); }, opts.timeout);
121
144
  });
122
145
  }
146
+ function validateStructuredCommand(command) {
147
+ if (!command || typeof command !== 'object' || typeof command.executable !== 'string' || (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(command.executable) && !path_1.default.isAbsolute(command.executable))) {
148
+ throw new Error('structured command executable must be a safe executable name');
149
+ }
150
+ if (!Array.isArray(command.args) || command.args.some(arg => typeof arg !== 'string' || /[\0\r\n]/.test(arg))) {
151
+ throw new Error('structured command args must be an array of single-line strings without NUL');
152
+ }
153
+ if (!['node-modules-bin', 'python-environment', 'path'].includes(command.resolution))
154
+ throw new Error('structured command resolution is unsupported');
155
+ }
156
+ function regularFile(candidate) {
157
+ try {
158
+ const stat = fs_1.default.lstatSync(candidate);
159
+ return stat.isFile() && !stat.isSymbolicLink();
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ function containedPath(root, candidate) {
166
+ const relative = path_1.default.relative(root, candidate);
167
+ return relative !== '' && !relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative);
168
+ }
169
+ /** Resolve a local npm shim to its real regular-file target. npm commonly uses
170
+ * symlinks in .bin on POSIX, so rejecting every symlink would reject valid local
171
+ * installs. The real target must remain inside the project's node_modules. */
172
+ function localNodeModulesExecutable(candidate, modulesRoot) {
173
+ try {
174
+ const shim = fs_1.default.lstatSync(candidate);
175
+ if (!shim.isFile() && !shim.isSymbolicLink())
176
+ return null;
177
+ const real = fs_1.default.realpathSync(candidate);
178
+ const target = fs_1.default.statSync(real);
179
+ return target.isFile() && containedPath(modulesRoot, real) ? real : null;
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ /** Find a real executable without a shell. Windows .cmd/.bat shims are deliberately
186
+ * excluded: CreateProcess cannot execute them safely without cmd.exe. */
187
+ function resolveStructuredExecutable(command, cwd) {
188
+ if (command.resolution === 'node-modules-bin') {
189
+ let modulesRoot;
190
+ try {
191
+ modulesRoot = fs_1.default.realpathSync(path_1.default.join(cwd, 'node_modules'));
192
+ if (!fs_1.default.statSync(modulesRoot).isDirectory())
193
+ throw new Error();
194
+ }
195
+ catch {
196
+ throw new Error('node_modules executable not found locally');
197
+ }
198
+ const bin = path_1.default.join(modulesRoot, '.bin');
199
+ if (!(0, paths_1.isWindowsNative)()) {
200
+ const local = localNodeModulesExecutable(path_1.default.join(bin, command.executable), modulesRoot);
201
+ if (local)
202
+ return local;
203
+ throw new Error('node_modules executable is not a contained local file');
204
+ }
205
+ const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map(ext => ext.toLowerCase()).filter(ext => ext === '.exe' || ext === '.com');
206
+ const candidates = [path_1.default.join(bin, command.executable), ...extensions.map(extension => path_1.default.join(bin, command.executable + extension))];
207
+ for (const candidate of candidates) {
208
+ const lower = candidate.toLowerCase();
209
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
210
+ throw new Error('structured commands cannot execute Windows command wrappers');
211
+ const local = localNodeModulesExecutable(candidate, modulesRoot);
212
+ if (local && extensions.some(extension => local.toLowerCase().endsWith(extension)))
213
+ return local;
214
+ }
215
+ throw new Error('node_modules executable is not a contained local file');
216
+ }
217
+ const candidates = [];
218
+ if (command.resolution === 'python-environment') {
219
+ if (path_1.default.isAbsolute(command.executable))
220
+ throw new Error('python environment executable must be a contained local name');
221
+ candidates.push(path_1.default.join(cwd, '.venv', (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', command.executable));
222
+ candidates.push(path_1.default.join(cwd, 'venv', (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', command.executable));
223
+ }
224
+ else if (path_1.default.isAbsolute(command.executable))
225
+ candidates.push(command.executable);
226
+ else {
227
+ for (const entry of (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean))
228
+ candidates.push(path_1.default.join(entry, command.executable));
229
+ }
230
+ if (!(0, paths_1.isWindowsNative)()) {
231
+ const found = candidates.find(regularFile);
232
+ if (found)
233
+ return found;
234
+ if (command.resolution === 'python-environment')
235
+ throw new Error('python environment executable is not a contained local regular file');
236
+ return command.executable;
237
+ }
238
+ const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map(ext => ext.toLowerCase()).filter(ext => ext === '.exe' || ext === '.com');
239
+ for (const candidate of candidates) {
240
+ const lower = candidate.toLowerCase();
241
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
242
+ throw new Error('structured commands cannot execute Windows command wrappers');
243
+ if (regularFile(candidate) && extensions.some(ext => lower.endsWith(ext)))
244
+ return candidate;
245
+ for (const extension of extensions)
246
+ if (regularFile(candidate + extension))
247
+ return candidate + extension;
248
+ }
249
+ if (command.resolution === 'python-environment')
250
+ throw new Error('python environment executable is not a contained local regular file');
251
+ return command.executable;
252
+ }
253
+ /** Execute a v2 command as an executable plus literal argv; it never starts a shell. */
254
+ function runStructuredCommand(command, opts) {
255
+ validateStructuredCommand(command);
256
+ return collectSpawn({ executable: resolveStructuredExecutable(command, opts.cwd), args: command.args, shell: false }, opts);
257
+ }
258
+ /** Legacy sensor strings intentionally retain their documented shell semantics. */
259
+ function runCommand(cmd, opts) {
260
+ if (typeof cmd !== 'string' || cmd.trim() === '' || /[\0\r\n]/.test(cmd))
261
+ throw new Error('runCommand: cmd must be a non-empty single-line legacy string without NUL');
262
+ return collectSpawn({ executable: cmd, args: [], shell: true }, opts);
263
+ }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.exitCodeFor = exitCodeFor;
7
+ exports.parsePositiveSafeInteger = parsePositiveSafeInteger;
7
8
  exports.registerSensorsCommand = registerSensorsCommand;
8
9
  const picocolors_1 = __importDefault(require("picocolors"));
9
10
  const prompts_1 = require("@clack/prompts");
@@ -21,15 +22,26 @@ const registries_1 = require("../../core/registries");
21
22
  function exitCodeFor(output) {
22
23
  return output.overall === 'fail' ? 1 : 0;
23
24
  }
25
+ /** Commander coercion for coverage recurrence emphasis. It deliberately runs
26
+ * before the action, so an invalid value cannot trigger ledger I/O. */
27
+ function parsePositiveSafeInteger(value) {
28
+ if (typeof value !== 'string' || !/^[1-9][0-9]*$/.test(value))
29
+ throw new Error('--min must be a positive safe integer');
30
+ const parsed = Number(value);
31
+ if (!Number.isSafeInteger(parsed))
32
+ throw new Error('--min must be a positive safe integer');
33
+ return parsed;
34
+ }
24
35
  function registerSensorsCommand(program) {
25
36
  const sensors = program.command('sensors').description('manage computational sensors for the current project');
26
37
  sensors
27
38
  .command('coverage')
28
39
  .description('report static gaps between configured sensors and the pack reference')
29
40
  .option('--json', 'emit the versioned machine-readable envelope')
30
- .action((opts) => {
41
+ .option('--min <count>', 'recurrence emphasis threshold', parsePositiveSafeInteger, 2)
42
+ .action(async (opts) => {
31
43
  try {
32
- const report = (0, coverage_1.runCoverage)(process.cwd());
44
+ const report = await (0, coverage_1.runCoverage)(process.cwd(), {}, { min: opts.min });
33
45
  process.stdout.write(opts.json ? (0, render_1.renderCoverageJson)(report) : (0, render_1.renderCoverageHuman)(report));
34
46
  }
35
47
  catch (error) {
@@ -63,10 +75,10 @@ function registerSensorsCommand(program) {
63
75
  .option('--no-configure', 'skip copying sensor pack config files into the project')
64
76
  .option('--registry-root <path>', 'path to AWM registry root')
65
77
  .option('--pack <name>', 'skip auto-detection, use this pack explicitly')
66
- .action((opts) => {
78
+ .action(async (opts) => {
67
79
  const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
68
80
  try {
69
- const result = (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
81
+ const result = await (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
70
82
  prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
71
83
  // Said BEFORE "Wrote .awm/sensors.json": the manifest about to be
72
84
  // reported as written is not the one the detection implied.
@@ -99,8 +111,8 @@ function registerSensorsCommand(program) {
99
111
  sensors
100
112
  .command('status')
101
113
  .description('check sensor health for the current project')
102
- .action(() => {
103
- const status = (0, status_1.computeSensorStatus)();
114
+ .action(async () => {
115
+ const status = await (0, status_1.computeSensorStatus)();
104
116
  const icon = status.overall === 'HEALTHY' ? picocolors_1.default.green('✔') : picocolors_1.default.yellow('⚠');
105
117
  console.log(`\nPack: ${status.pack ?? 'none'}`);
106
118
  console.log(`Overall: ${icon} ${status.overall}\n`);