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
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseSemgrepPolicy = parseSemgrepPolicy;
7
+ exports.resolveSemgrepPolicy = resolveSemgrepPolicy;
8
+ exports.parseStructuredCommand = parseStructuredCommand;
9
+ exports.assertNoEqualPriorityOverlap = assertNoEqualPriorityOverlap;
10
+ exports.parseSensorPack = parseSensorPack;
11
+ const semver_1 = __importDefault(require("semver"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const contract_1 = require("../coverage/contract");
15
+ const PACK_SCHEMA_VERSION = 2;
16
+ const SHELL_EXECUTABLES = new Set(['sh', 'bash', 'cmd', 'powershell']);
17
+ const PACKAGE_MANAGERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
18
+ const ALLOWED_PROBES = new Set([
19
+ 'version',
20
+ 'eslint-print-config',
21
+ 'typescript-show-config',
22
+ 'semgrep-validate',
23
+ 'package-script-present',
24
+ 'config-present',
25
+ ]);
26
+ function normalizedPackageManager(value) {
27
+ return value.toLowerCase().replace(/\.exe$/, '');
28
+ }
29
+ function isRecord(value) {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+ function sourceSuffix(source) {
33
+ return typeof source === 'string' && source.length > 0 && !/[\0\r\n]/.test(source) ? ` in ${source}` : ' in <unknown source>';
34
+ }
35
+ function invalid(source, message) {
36
+ throw new Error(`Invalid sensor pack${sourceSuffix(source)}: ${message}`);
37
+ }
38
+ function record(value, source, location) {
39
+ if (!isRecord(value))
40
+ invalid(source, `${location} must be an object`);
41
+ return value;
42
+ }
43
+ function fields(value, allowed, source, location) {
44
+ for (const key of Object.keys(value)) {
45
+ if (!allowed.includes(key))
46
+ invalid(source, `${location} has unknown field "${key}"`);
47
+ }
48
+ }
49
+ function text(value, source, location) {
50
+ if (typeof value !== 'string' || value.trim().length === 0 || /[\0\r\n]/.test(value)) {
51
+ invalid(source, `${location} must be a nonempty single-line string without NUL`);
52
+ }
53
+ return value;
54
+ }
55
+ function id(value, source, location) {
56
+ const parsed = text(value, source, location);
57
+ if (!/^[a-z][a-z0-9-]*$/.test(parsed))
58
+ invalid(source, `${location} must be a stable lowercase id`);
59
+ return parsed;
60
+ }
61
+ function asset(value, source, location) {
62
+ const parsed = text(value, source, location);
63
+ if (parsed.startsWith('/') || /^[A-Za-z]:[\\/]/.test(parsed) || parsed.startsWith('\\\\') || parsed.includes('\\') || parsed.split('/').some(part => part === '' || part === '.' || part === '..')) {
64
+ invalid(source, `${location} must be a contained relative asset path`);
65
+ }
66
+ return parsed;
67
+ }
68
+ function stringArray(value, source, location) {
69
+ if (!Array.isArray(value) || value.length === 0)
70
+ invalid(source, `${location} must be a nonempty array`);
71
+ return value.map((item, index) => text(item, source, `${location}[${index}]`));
72
+ }
73
+ function assetArray(value, source, location, allowEmpty = false) {
74
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0))
75
+ invalid(source, `${location} must be a ${allowEmpty ? '' : 'nonempty '}array`);
76
+ return value.map((item, index) => asset(item, source, `${location}[${index}]`));
77
+ }
78
+ const SEMGREP_POLICY_REF = 'shared/semgrep-policy.json';
79
+ const MAX_POLICY_BYTES = 64 * 1024;
80
+ function contained(root, candidate) {
81
+ const prefix = root.endsWith(path_1.default.sep) ? root : root + path_1.default.sep;
82
+ return candidate.startsWith(prefix);
83
+ }
84
+ function parseSemgrepPolicy(input, source) {
85
+ const value = record(input, source, 'Semgrep policy');
86
+ fields(value, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'probe'], source, 'Semgrep policy');
87
+ if (value.tool !== 'semgrep' || value.runtime !== 'python' || value.probe !== 'semgrep-validate') {
88
+ invalid(source, 'Semgrep policy must declare semgrep, python, and semgrep-validate');
89
+ }
90
+ const toolRange = text(value.toolRange, source, 'Semgrep policy.toolRange');
91
+ const runtimeRange = text(value.runtimeRange, source, 'Semgrep policy.runtimeRange');
92
+ if (semver_1.default.validRange(toolRange) === null || semver_1.default.validRange(runtimeRange) === null)
93
+ invalid(source, 'Semgrep policy ranges must be valid semver ranges');
94
+ return { tool: 'semgrep', toolRange, runtime: 'python', runtimeRange, probe: 'semgrep-validate' };
95
+ }
96
+ function resolveSemgrepPolicy(policyRef, source, location = 'policy') {
97
+ if (policyRef !== SEMGREP_POLICY_REF)
98
+ invalid(source, `${location}.policyRef must be the contained AWM-owned ${SEMGREP_POLICY_REF}`);
99
+ if (typeof source !== 'string' || !path_1.default.isAbsolute(source) || path_1.default.normalize(source) !== source || path_1.default.basename(source) !== 'pack.json') {
100
+ invalid(source, `${location}.policyRef requires an absolute pack.json source`);
101
+ }
102
+ const packRoot = path_1.default.dirname(source);
103
+ const sensorPacksRoot = path_1.default.dirname(packRoot);
104
+ const candidate = path_1.default.join(sensorPacksRoot, ...SEMGREP_POLICY_REF.split('/'));
105
+ if (!contained(sensorPacksRoot, candidate))
106
+ invalid(source, `${location}.policyRef escapes sensor-packs root`);
107
+ let stat;
108
+ try {
109
+ stat = fs_1.default.lstatSync(candidate);
110
+ }
111
+ catch {
112
+ invalid(source, `${location}.policyRef does not resolve to a regular policy file`);
113
+ }
114
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_POLICY_BYTES)
115
+ invalid(source, `${location}.policyRef must resolve to a bounded regular policy file`);
116
+ let realRoot;
117
+ let realPolicy;
118
+ try {
119
+ realRoot = fs_1.default.realpathSync(sensorPacksRoot);
120
+ realPolicy = fs_1.default.realpathSync(candidate);
121
+ }
122
+ catch {
123
+ invalid(source, `${location}.policyRef cannot be canonicalized`);
124
+ }
125
+ if (!contained(realRoot, realPolicy))
126
+ invalid(source, `${location}.policyRef escapes sensor-packs root`);
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(fs_1.default.readFileSync(realPolicy, 'utf8'));
130
+ }
131
+ catch {
132
+ invalid(source, `${location}.policyRef must contain valid JSON`);
133
+ }
134
+ return parseSemgrepPolicy(parsed, realPolicy);
135
+ }
136
+ function parseStructuredCommand(input, source) {
137
+ const value = record(input, source, 'command');
138
+ fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source, 'command');
139
+ const executable = text(value.executable, source, 'command.executable');
140
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(executable) || SHELL_EXECUTABLES.has(executable.toLowerCase().replace(/\.exe$/, ''))) {
141
+ invalid(source, 'command.executable must not be a shell or path');
142
+ }
143
+ if (value.resolution !== 'node-modules-bin' && value.resolution !== 'python-environment' && value.resolution !== 'path') {
144
+ invalid(source, 'command.resolution must be a supported resolution');
145
+ }
146
+ const args = stringArray(value.args, source, 'command.args');
147
+ for (const [index, arg] of args.entries()) {
148
+ if (arg.includes('{files}') && arg !== '{files}')
149
+ invalid(source, `command.args[${index}] must not embed {files}`);
150
+ }
151
+ const command = { executable, resolution: value.resolution, args };
152
+ if ('pythonEnvironmentRoot' in value) {
153
+ if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv'))
154
+ invalid(source, 'command.pythonEnvironmentRoot must name the selected .venv or venv Python environment');
155
+ command.pythonEnvironmentRoot = value.pythonEnvironmentRoot;
156
+ }
157
+ const executablePackageManager = normalizedPackageManager(executable);
158
+ if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value))
159
+ invalid(source, 'command.packageManager is required for a package-manager executable');
160
+ if ('packageManager' in value) {
161
+ if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager)))
162
+ invalid(source, 'command.packageManager must be npm, pnpm, yarn, or bun');
163
+ const packageManager = normalizedPackageManager(value.packageManager);
164
+ if (packageManager !== executablePackageManager)
165
+ invalid(source, 'command.packageManager must match executable');
166
+ command.packageManager = packageManager;
167
+ }
168
+ if ('environment' in value) {
169
+ const environment = record(value.environment, source, 'command.environment');
170
+ fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source, 'command.environment');
171
+ if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1)
172
+ invalid(source, 'command.environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
173
+ command.environment = { ESLINT_USE_FLAT_CONFIG: environment.ESLINT_USE_FLAT_CONFIG };
174
+ }
175
+ if ('fileInput' in value) {
176
+ const fileInput = record(value.fileInput, source, 'command.fileInput');
177
+ fields(fileInput, ['placeholder', 'extensions'], source, 'command.fileInput');
178
+ if (fileInput.placeholder !== '{files}')
179
+ invalid(source, 'command.fileInput.placeholder must be {files}');
180
+ const extensions = stringArray(fileInput.extensions, source, 'command.fileInput.extensions');
181
+ for (const [index, extension] of extensions.entries()) {
182
+ if (!/^\.[A-Za-z0-9]+$/.test(extension))
183
+ invalid(source, `command.fileInput.extensions[${index}] must be an extension`);
184
+ }
185
+ if (args.filter(arg => arg === '{files}').length !== 1)
186
+ invalid(source, 'command.fileInput requires exactly one {files} argument');
187
+ command.fileInput = { placeholder: '{files}', extensions };
188
+ }
189
+ else if (args.includes('{files}')) {
190
+ invalid(source, 'command {files} argument requires fileInput');
191
+ }
192
+ return command;
193
+ }
194
+ function parseVariant(input, source, location) {
195
+ const value = record(input, source, location);
196
+ fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'assets', 'formatter', 'probe', 'policyRef'], source, location);
197
+ const certifiedRange = text(value.certifiedRange, source, `${location}.certifiedRange`);
198
+ if (semver_1.default.validRange(certifiedRange) === null)
199
+ invalid(source, `${location}.certifiedRange must be a valid semver range`);
200
+ if (typeof value.priority !== 'number' || !Number.isSafeInteger(value.priority))
201
+ invalid(source, `${location}.priority must be a safe integer`);
202
+ const policy = 'policyRef' in value ? resolveSemgrepPolicy(value.policyRef, source, location) : undefined;
203
+ if (policy && ('requirements' in value || 'probe' in value))
204
+ invalid(source, `${location}.policyRef is authoritative; requirements and probe must not duplicate it`);
205
+ if (!policy && (!('requirements' in value) || !('probe' in value)))
206
+ invalid(source, `${location} requires requirements and probe when policyRef is absent`);
207
+ const requirements = policy ? undefined : record(value.requirements, source, `${location}.requirements`);
208
+ if (requirements)
209
+ fields(requirements, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'configFiles'], source, `${location}.requirements`);
210
+ const toolRange = policy?.toolRange ?? text(requirements.toolRange, source, `${location}.requirements.toolRange`);
211
+ const runtimeRange = policy?.runtimeRange ?? text(requirements.runtimeRange, source, `${location}.requirements.runtimeRange`);
212
+ if (semver_1.default.validRange(toolRange) === null || semver_1.default.validRange(runtimeRange) === null)
213
+ invalid(source, `${location}.requirements ranges must be valid semver ranges`);
214
+ const probe = policy ? undefined : record(value.probe, source, `${location}.probe`);
215
+ if (probe) {
216
+ fields(probe, ['kind'], source, `${location}.probe`);
217
+ if (typeof probe.kind !== 'string' || !ALLOWED_PROBES.has(probe.kind))
218
+ invalid(source, `${location}.probe.kind must be an allowed probe`);
219
+ }
220
+ return {
221
+ id: id(value.id, source, `${location}.id`),
222
+ priority: value.priority,
223
+ certifiedRange,
224
+ requirements: policy
225
+ ? { tool: policy.tool, toolRange, runtime: policy.runtime, runtimeRange }
226
+ : { tool: text(requirements.tool, source, `${location}.requirements.tool`), toolRange, runtime: text(requirements.runtime, source, `${location}.requirements.runtime`), runtimeRange, ...('configFiles' in requirements ? { configFiles: stringArray(requirements.configFiles, source, `${location}.requirements.configFiles`).map((file, index) => asset(file, source, `${location}.requirements.configFiles[${index}]`)) } : {}) },
227
+ assets: assetArray(value.assets, source, `${location}.assets`, true),
228
+ formatter: text(value.formatter, source, `${location}.formatter`),
229
+ probe: { kind: policy?.probe ?? probe.kind },
230
+ ...(policy ? { policyRef: SEMGREP_POLICY_REF } : {}),
231
+ command: parseStructuredCommand(value.command, source),
232
+ };
233
+ }
234
+ function parseHardening(input, source) {
235
+ const value = record(input, source, 'hardening');
236
+ const names = Object.keys(value);
237
+ if (names.length === 0)
238
+ invalid(source, 'hardening must be nonempty when declared');
239
+ const hardening = {};
240
+ for (const name of names) {
241
+ const entry = record(value[name], source, `hardening.${name}`);
242
+ fields(entry, ['assets'], source, `hardening.${name}`);
243
+ hardening[id(name, source, 'hardening id')] = { assets: assetArray(entry.assets, source, `hardening.${name}.assets`) };
244
+ }
245
+ return hardening;
246
+ }
247
+ function assertNoEqualPriorityOverlapParsed(parsed) {
248
+ for (let left = 0; left < parsed.length; left++) {
249
+ const first = parsed[left];
250
+ for (let right = left + 1; right < parsed.length; right++) {
251
+ const second = parsed[right];
252
+ const toolRangesIntersect = semver_1.default.intersects(first.requirements.toolRange, second.requirements.toolRange);
253
+ const runtimeRangesIntersect = semver_1.default.intersects(first.requirements.runtimeRange, second.requirements.runtimeRange);
254
+ if (first.priority === second.priority && toolRangesIntersect && runtimeRangesIntersect) {
255
+ throw new Error(`variants "${first.id}" and "${second.id}" overlap at priority ${first.priority}`);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ function assertNoEqualPriorityOverlap(variants) {
261
+ if (!Array.isArray(variants) || variants.length === 0)
262
+ throw new Error('variants must be a nonempty array');
263
+ assertNoEqualPriorityOverlapParsed(variants.map((variant, index) => parseVariant(variant, 'public overlap validator', `variants[${index}]`)));
264
+ }
265
+ function parseSensor(input, source, location, variantIds) {
266
+ const value = record(input, source, location);
267
+ fields(value, ['applicability', 'variants', 'fast'], source, location);
268
+ if (!Array.isArray(value.variants) || value.variants.length === 0)
269
+ invalid(source, `${location}.variants must be a nonempty array`);
270
+ const variants = value.variants.map((variant, index) => parseVariant(variant, source, `${location}.variants[${index}]`));
271
+ for (const variant of variants) {
272
+ if (variantIds.has(variant.id))
273
+ invalid(source, `variant id "${variant.id}" must be unique`);
274
+ variantIds.add(variant.id);
275
+ }
276
+ try {
277
+ assertNoEqualPriorityOverlapParsed(variants);
278
+ }
279
+ catch (error) {
280
+ invalid(source, `${location}.variants ${error instanceof Error ? error.message : 'overlap validation failed'}`);
281
+ }
282
+ const applicabilityInput = record(value.applicability, source, `${location}.applicability`);
283
+ fields(applicabilityInput, ['allFiles', 'anyFiles', 'kind'], source, `${location}.applicability`);
284
+ const applicability = {};
285
+ if ('allFiles' in applicabilityInput)
286
+ applicability.allFiles = stringArray(applicabilityInput.allFiles, source, `${location}.applicability.allFiles`).map((file, index) => asset(file, source, `${location}.applicability.allFiles[${index}]`));
287
+ if ('anyFiles' in applicabilityInput)
288
+ applicability.anyFiles = stringArray(applicabilityInput.anyFiles, source, `${location}.applicability.anyFiles`).map((file, index) => asset(file, source, `${location}.applicability.anyFiles[${index}]`));
289
+ if ('kind' in applicabilityInput)
290
+ applicability.kind = text(applicabilityInput.kind, source, `${location}.applicability.kind`);
291
+ if (Object.keys(applicability).length === 0)
292
+ invalid(source, `${location}.applicability must declare a condition`);
293
+ if ('fast' in value && typeof value.fast !== 'boolean')
294
+ invalid(source, `${location}.fast must be a boolean`);
295
+ return { applicability, variants, ...('fast' in value ? { fast: value.fast } : {}) };
296
+ }
297
+ function legacyCompatibility() {
298
+ return {
299
+ state: 'compatible-unverified',
300
+ reason: 'legacy pack without schemaVersion',
301
+ variantId: null,
302
+ toolVersion: null,
303
+ runtimeVersion: null,
304
+ certifiedRange: null,
305
+ evidence: [],
306
+ };
307
+ }
308
+ function parseLegacyPack(value, source) {
309
+ fields(value, ['name', 'description', 'detects', 'sensors', 'coverage'], source, 'root');
310
+ const name = id(value.name, source, 'name');
311
+ const sensorsInput = record(value.sensors, source, 'sensors');
312
+ const sensors = {};
313
+ for (const sensorName of Object.keys(sensorsInput)) {
314
+ const sensor = record(sensorsInput[sensorName], source, `sensors.${sensorName}`);
315
+ fields(sensor, ['defaultCmd', 'fast', 'enabled', 'changedCmd', 'changedExtensions', 'formatter', 'configFile', 'configFileFallback'], source, `sensors.${sensorName}`);
316
+ const parsed = {};
317
+ if ('defaultCmd' in sensor)
318
+ parsed.defaultCmd = text(sensor.defaultCmd, source, `sensors.${sensorName}.defaultCmd`);
319
+ if ('fast' in sensor) {
320
+ if (typeof sensor.fast !== 'boolean')
321
+ invalid(source, `sensors.${sensorName}.fast must be a boolean`);
322
+ parsed.fast = sensor.fast;
323
+ }
324
+ if ('enabled' in sensor) {
325
+ if (typeof sensor.enabled !== 'boolean')
326
+ invalid(source, `sensors.${sensorName}.enabled must be a boolean`);
327
+ parsed.enabled = sensor.enabled;
328
+ }
329
+ if ('changedCmd' in sensor)
330
+ parsed.changedCmd = text(sensor.changedCmd, source, `sensors.${sensorName}.changedCmd`);
331
+ if ('changedExtensions' in sensor)
332
+ parsed.changedExtensions = stringArray(sensor.changedExtensions, source, `sensors.${sensorName}.changedExtensions`);
333
+ if ('formatter' in sensor)
334
+ parsed.formatter = text(sensor.formatter, source, `sensors.${sensorName}.formatter`);
335
+ if ('configFile' in sensor)
336
+ parsed.configFile = asset(sensor.configFile, source, `sensors.${sensorName}.configFile`);
337
+ if ('configFileFallback' in sensor)
338
+ parsed.configFileFallback = asset(sensor.configFileFallback, source, `sensors.${sensorName}.configFileFallback`);
339
+ sensors[id(sensorName, source, 'sensor id')] = parsed;
340
+ }
341
+ const legacy = { name, sensors, compatibility: legacyCompatibility() };
342
+ if ('description' in value)
343
+ legacy.description = text(value.description, source, 'description');
344
+ if ('detects' in value) {
345
+ if (!Array.isArray(value.detects))
346
+ invalid(source, 'detects must be an array');
347
+ legacy.detects = value.detects.map((detect, index) => text(detect, source, `detects[${index}]`));
348
+ }
349
+ if ('coverage' in value)
350
+ legacy.coverage = (0, contract_1.parseCoverageContract)(value.coverage, source);
351
+ return legacy;
352
+ }
353
+ function parseSensorPack(input, source) {
354
+ const value = record(input, source, 'root');
355
+ if (!('schemaVersion' in value))
356
+ return { kind: 'legacy', pack: parseLegacyPack(value, source) };
357
+ fields(value, ['schemaVersion', 'name', 'description', 'detects', 'sensors', 'coverage', 'hardening'], source, 'root');
358
+ if (value.schemaVersion !== PACK_SCHEMA_VERSION)
359
+ invalid(source, `unsupported pack schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2; upgrade or migrate the pack`);
360
+ const sensorsInput = record(value.sensors, source, 'sensors');
361
+ const sensorNames = Object.keys(sensorsInput);
362
+ if (sensorNames.length === 0)
363
+ invalid(source, 'sensors must be nonempty');
364
+ const sensors = {};
365
+ const variantIds = new Set();
366
+ for (const name of sensorNames)
367
+ sensors[id(name, source, 'sensor id')] = parseSensor(sensorsInput[name], source, `sensors.${name}`, variantIds);
368
+ let coverage;
369
+ try {
370
+ coverage = (0, contract_1.parseCoverageContract)(value.coverage, source);
371
+ }
372
+ catch (error) {
373
+ invalid(source, `coverage.${error instanceof Error ? error.message.replace(/^.*?: /, '') : 'is invalid'}`);
374
+ }
375
+ const hardening = 'hardening' in value ? parseHardening(value.hardening, source) : undefined;
376
+ return { kind: 'v2', pack: {
377
+ schemaVersion: PACK_SCHEMA_VERSION,
378
+ name: id(value.name, source, 'name'), description: text(value.description, source, 'description'), detects: stringArray(value.detects, source, 'detects'),
379
+ sensors, ...(hardening ? { hardening } : {}),
380
+ coverage,
381
+ } };
382
+ }
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.discoverProjectEvidence = discoverProjectEvidence;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const semver_1 = __importDefault(require("semver"));
10
+ const paths_1 = require("../../../core/paths");
11
+ const LOCKFILES = { 'package-lock.json': 'npm', 'npm-shrinkwrap.json': 'npm', 'pnpm-lock.yaml': 'pnpm', 'yarn.lock': 'yarn', 'bun.lockb': 'bun', 'bun.lock': 'bun' };
12
+ const COMMON_CONFIGS = ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', '.eslintrc', '.eslintrc.json', 'tsconfig.json', 'semgrep.yml', 'semgrep.yaml', '.semgrep.yml', 'pyproject.toml', 'ruff.toml', 'mypy.ini', 'setup.cfg'];
13
+ function safeFile(root, relative) {
14
+ if (!relative || path_1.default.isAbsolute(relative) || relative.includes('\\') || relative.split('/').some(part => part === '.' || part === '..' || part === ''))
15
+ return false;
16
+ try {
17
+ const stat = fs_1.default.lstatSync(path_1.default.join(root, relative));
18
+ return stat.isFile() && !stat.isSymbolicLink();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function safeParts(parts) {
25
+ return parts.length > 0 && parts.every(part => Boolean(part) && part !== '.' && part !== '..' && !part.includes('/') && !part.includes('\\'));
26
+ }
27
+ /** Refuse every symlink in a local environment path; evidence must never escape cwd. */
28
+ function containedEntry(root, parts, expect) {
29
+ if (!safeParts(parts))
30
+ return null;
31
+ let candidate = root;
32
+ try {
33
+ for (const part of parts) {
34
+ candidate = path_1.default.join(candidate, part);
35
+ const stat = fs_1.default.lstatSync(candidate);
36
+ if (stat.isSymbolicLink())
37
+ return null;
38
+ }
39
+ const stat = fs_1.default.lstatSync(candidate);
40
+ return (expect === 'file' ? stat.isFile() : stat.isDirectory()) ? candidate : null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function readBoundedLocalFile(root, parts) {
47
+ const file = containedEntry(root, parts, 'file');
48
+ if (!file)
49
+ return null;
50
+ try {
51
+ const stat = fs_1.default.statSync(file);
52
+ return stat.size <= 64 * 1024 ? fs_1.default.readFileSync(file, 'utf8') : null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function exactVersion(value) {
59
+ return typeof value === 'string' && semver_1.default.valid(value.trim()) !== null ? semver_1.default.clean(value.trim()) : null;
60
+ }
61
+ function pythonEnvironment(root) {
62
+ for (const name of ['.venv', 'venv']) {
63
+ if (!containedEntry(root, [name], 'directory'))
64
+ continue;
65
+ const config = readBoundedLocalFile(root, [name, 'pyvenv.cfg']);
66
+ const version = config?.match(/^version\s*=\s*([^\r\n#]+)\s*$/mi)?.[1] ?? null;
67
+ return { rootParts: [name], runtimeVersion: exactVersion(version) };
68
+ }
69
+ return null;
70
+ }
71
+ function pythonSitePackages(root, environment, target) {
72
+ const candidates = target === 'win32'
73
+ ? [[...environment, 'Lib', 'site-packages']]
74
+ : [];
75
+ if (target !== 'win32') {
76
+ const lib = containedEntry(root, [...environment, 'lib'], 'directory');
77
+ if (lib) {
78
+ try {
79
+ for (const entry of fs_1.default.readdirSync(lib, { withFileTypes: true }).slice(0, 128)) {
80
+ if (entry.isDirectory() && !entry.isSymbolicLink() && /^python\d+\.\d+$/.test(entry.name))
81
+ candidates.push([...environment, 'lib', entry.name, 'site-packages']);
82
+ }
83
+ }
84
+ catch { /* unreadable environments are not evidence */ }
85
+ }
86
+ }
87
+ return candidates.filter(parts => containedEntry(root, parts, 'directory') !== null);
88
+ }
89
+ function pythonToolVersion(root, sitePackages, tool) {
90
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(tool))
91
+ return null;
92
+ const normalized = tool.replace(/[._-]+/g, '-').toLowerCase();
93
+ for (const directory of sitePackages) {
94
+ const folder = containedEntry(root, directory, 'directory');
95
+ if (!folder)
96
+ continue;
97
+ let entries;
98
+ try {
99
+ entries = fs_1.default.readdirSync(folder, { withFileTypes: true }).slice(0, 512);
100
+ }
101
+ catch {
102
+ continue;
103
+ }
104
+ for (const entry of entries) {
105
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !entry.name.endsWith('.dist-info'))
106
+ continue;
107
+ const distribution = entry.name.slice(0, -'.dist-info'.length);
108
+ if (!distribution.toLowerCase().startsWith(normalized + '-'))
109
+ continue;
110
+ const metadata = readBoundedLocalFile(root, [...directory, entry.name, 'METADATA']);
111
+ if (!metadata)
112
+ continue;
113
+ const name = metadata.match(/^Name:\s*([^\r\n]+)\s*$/mi)?.[1]?.trim().replace(/[._-]+/g, '-').toLowerCase();
114
+ const version = exactVersion(metadata.match(/^Version:\s*([^\r\n]+)\s*$/mi)?.[1]);
115
+ if (name === normalized && version)
116
+ return version;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ function packageJson(root) {
122
+ if (!safeFile(root, 'package.json'))
123
+ return null;
124
+ try {
125
+ const parsed = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf8'));
126
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ function stringMap(value) {
133
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
134
+ return {};
135
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
136
+ }
137
+ function installedPackageVersion(root, tool) {
138
+ if (!tool || tool.split('/').some(part => !part || part === '.' || part === '..' || part.includes('\\')))
139
+ return null;
140
+ const packageFile = path_1.default.join(root, 'node_modules', ...tool.split('/'), 'package.json');
141
+ try {
142
+ const modules = fs_1.default.realpathSync(path_1.default.join(root, 'node_modules'));
143
+ const fileStat = fs_1.default.lstatSync(packageFile);
144
+ if (!fileStat.isFile() || fileStat.isSymbolicLink())
145
+ return null;
146
+ const real = fs_1.default.realpathSync(packageFile);
147
+ if (!real.startsWith(modules + path_1.default.sep))
148
+ return null;
149
+ const parsed = JSON.parse(fs_1.default.readFileSync(real, 'utf8'));
150
+ const version = typeof parsed.version === 'string' ? parsed.version : null;
151
+ return version !== null && semver_1.default.valid(version) !== null ? version : null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ /** Read bounded project metadata only; it never shells out, downloads, or mutates. */
158
+ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
159
+ if (typeof cwd !== 'string' || cwd.trim() === '')
160
+ throw new Error('cwd must be a non-empty path');
161
+ let root;
162
+ try {
163
+ root = fs_1.default.realpathSync(cwd);
164
+ if (!fs_1.default.statSync(root).isDirectory())
165
+ throw new Error();
166
+ }
167
+ catch {
168
+ throw new Error(`cwd is not a readable project directory: ${cwd}`);
169
+ }
170
+ if (!pack || typeof pack !== 'object' || typeof pack.name !== 'string')
171
+ throw new Error('pack must be a parsed sensor pack');
172
+ const targetPlatform = (dependencies.platform ?? paths_1.platform)();
173
+ const pkg = packageJson(root);
174
+ const locks = Object.keys(LOCKFILES).filter(file => safeFile(root, file));
175
+ const lockManagers = new Set(locks.map(file => LOCKFILES[file]));
176
+ const declaredManager = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
177
+ if (declaredManager)
178
+ lockManagers.add(declaredManager);
179
+ const configCandidates = new Set(COMMON_CONFIGS);
180
+ if ('schemaVersion' in pack) {
181
+ for (const marker of pack.detects)
182
+ configCandidates.add(marker);
183
+ for (const sensor of Object.values(pack.sensors)) {
184
+ for (const marker of [...(sensor.applicability.allFiles ?? []), ...(sensor.applicability.anyFiles ?? [])])
185
+ configCandidates.add(marker);
186
+ for (const variant of sensor.variants)
187
+ for (const config of variant.requirements.configFiles ?? [])
188
+ configCandidates.add(config);
189
+ }
190
+ }
191
+ const configFiles = [...configCandidates].filter(file => safeFile(root, file)).sort();
192
+ const scripts = Object.keys(stringMap(pkg?.scripts)).sort();
193
+ const declaredToolRanges = { ...stringMap(pkg?.dependencies), ...stringMap(pkg?.devDependencies), ...stringMap(pkg?.peerDependencies) };
194
+ const tools = new Set();
195
+ if ('schemaVersion' in pack)
196
+ for (const sensor of Object.values(pack.sensors))
197
+ for (const variant of sensor.variants)
198
+ tools.add(variant.requirements.tool);
199
+ const environment = pythonEnvironment(root);
200
+ const sitePackages = environment ? pythonSitePackages(root, environment.rootParts, targetPlatform) : [];
201
+ const toolVersions = Object.fromEntries([...tools].sort().map(tool => [tool, pythonToolVersion(root, sitePackages, tool) ?? installedPackageVersion(root, tool)]));
202
+ return {
203
+ cwd: root, os: targetPlatform, runtimeVersions: { node: process.versions.node ?? null, ...(environment ? { python: environment.runtimeVersion } : {}) }, pythonEnvironmentRoot: environment?.rootParts[0] ?? null, declaredToolRanges, toolVersions,
204
+ packageManager: declaredManager ?? (lockManagers.size === 1 ? [...lockManagers][0] : null), packageManagerConflict: lockManagers.size > 1,
205
+ scripts, configFiles, paths: [...new Set([...(safeFile(root, 'package.json') ? ['package.json'] : []), ...locks, ...configFiles])].sort(),
206
+ };
207
+ }