agentic-workflow-manager 9.16.1 → 9.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/env/index.js +192 -0
- package/dist/src/commands/preflight/checks.js +63 -13
- package/dist/src/commands/registry/doctor.js +428 -0
- package/dist/src/commands/registry/index.js +52 -0
- package/dist/src/commands/registry/scaffold.js +140 -0
- package/dist/src/commands/sensors/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- package/dist/src/commands/watch/index.js +3 -0
- package/dist/src/core/environments/resolve.js +122 -0
- package/dist/src/core/environments/store.js +323 -0
- package/dist/src/core/facts/catalog.js +138 -0
- package/dist/src/core/facts/needs.js +57 -0
- package/dist/src/core/facts/types.js +21 -0
- package/dist/src/core/init/steps.js +5 -1
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/preflight/preflight.test.js +121 -0
- package/dist/tests/commands/registry/doctor.test.js +283 -0
- package/dist/tests/commands/registry/scaffold.test.js +186 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -0
- package/dist/tests/commands/watch/reopened-196.test.js +9 -2
- package/dist/tests/core/environments/resolve.test.js +347 -0
- package/dist/tests/core/environments/store.test.js +248 -0
- package/dist/tests/core/facts/catalog.test.js +179 -0
- package/dist/tests/core/facts/needs.test.js +88 -0
- package/dist/tests/structural/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -137,61 +137,65 @@ function resolveSemgrepPolicy(policyRef, source, location = 'policy') {
|
|
|
137
137
|
}
|
|
138
138
|
return parseSemgrepPolicy(parsed, candidate);
|
|
139
139
|
}
|
|
140
|
-
function parseStructuredCommand(input, source) {
|
|
141
|
-
const value = record(input, source,
|
|
142
|
-
fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source,
|
|
143
|
-
const executable = text(value.executable, source,
|
|
140
|
+
function parseStructuredCommand(input, source, location = 'command') {
|
|
141
|
+
const value = record(input, source, location);
|
|
142
|
+
fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source, location);
|
|
143
|
+
const executable = text(value.executable, source, `${location}.executable`);
|
|
144
144
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(executable) || SHELL_EXECUTABLES.has(executable.toLowerCase().replace(/\.exe$/, ''))) {
|
|
145
|
-
invalid(source,
|
|
145
|
+
invalid(source, `${location}.executable must not be a shell or path`);
|
|
146
146
|
}
|
|
147
147
|
if (value.resolution !== 'node-modules-bin' && value.resolution !== 'python-environment' && value.resolution !== 'path') {
|
|
148
|
-
invalid(source,
|
|
148
|
+
invalid(source, `${location}.resolution must be a supported resolution`);
|
|
149
149
|
}
|
|
150
|
-
const args = stringArray(value.args, source,
|
|
150
|
+
const args = stringArray(value.args, source, `${location}.args`);
|
|
151
151
|
for (const [index, arg] of args.entries()) {
|
|
152
152
|
if (arg.includes('{files}') && arg !== '{files}')
|
|
153
|
-
invalid(source,
|
|
153
|
+
invalid(source, `${location}.args[${index}] must not embed {files}`);
|
|
154
154
|
}
|
|
155
155
|
const command = { executable, resolution: value.resolution, args };
|
|
156
156
|
if ('pythonEnvironmentRoot' in value) {
|
|
157
|
-
if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv'))
|
|
158
|
-
invalid(source,
|
|
157
|
+
if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv')) {
|
|
158
|
+
invalid(source, `${location}.pythonEnvironmentRoot must name the selected .venv or venv Python environment`);
|
|
159
|
+
}
|
|
159
160
|
command.pythonEnvironmentRoot = value.pythonEnvironmentRoot;
|
|
160
161
|
}
|
|
161
162
|
const executablePackageManager = normalizedPackageManager(executable);
|
|
162
|
-
if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value))
|
|
163
|
-
invalid(source,
|
|
163
|
+
if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value)) {
|
|
164
|
+
invalid(source, `${location}.packageManager is required for a package-manager executable`);
|
|
165
|
+
}
|
|
164
166
|
if ('packageManager' in value) {
|
|
165
|
-
if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager)))
|
|
166
|
-
invalid(source,
|
|
167
|
+
if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager))) {
|
|
168
|
+
invalid(source, `${location}.packageManager must be npm, pnpm, yarn, or bun`);
|
|
169
|
+
}
|
|
167
170
|
const packageManager = normalizedPackageManager(value.packageManager);
|
|
168
171
|
if (packageManager !== executablePackageManager)
|
|
169
|
-
invalid(source,
|
|
172
|
+
invalid(source, `${location}.packageManager must match executable`);
|
|
170
173
|
command.packageManager = packageManager;
|
|
171
174
|
}
|
|
172
175
|
if ('environment' in value) {
|
|
173
|
-
const environment = record(value.environment, source,
|
|
174
|
-
fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source,
|
|
175
|
-
if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1)
|
|
176
|
-
invalid(source,
|
|
176
|
+
const environment = record(value.environment, source, `${location}.environment`);
|
|
177
|
+
fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source, `${location}.environment`);
|
|
178
|
+
if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1) {
|
|
179
|
+
invalid(source, `${location}.environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping`);
|
|
180
|
+
}
|
|
177
181
|
command.environment = { ESLINT_USE_FLAT_CONFIG: environment.ESLINT_USE_FLAT_CONFIG };
|
|
178
182
|
}
|
|
179
183
|
if ('fileInput' in value) {
|
|
180
|
-
const fileInput = record(value.fileInput, source,
|
|
181
|
-
fields(fileInput, ['placeholder', 'extensions'], source,
|
|
184
|
+
const fileInput = record(value.fileInput, source, `${location}.fileInput`);
|
|
185
|
+
fields(fileInput, ['placeholder', 'extensions'], source, `${location}.fileInput`);
|
|
182
186
|
if (fileInput.placeholder !== '{files}')
|
|
183
|
-
invalid(source,
|
|
184
|
-
const extensions = stringArray(fileInput.extensions, source,
|
|
187
|
+
invalid(source, `${location}.fileInput.placeholder must be {files}`);
|
|
188
|
+
const extensions = stringArray(fileInput.extensions, source, `${location}.fileInput.extensions`);
|
|
185
189
|
for (const [index, extension] of extensions.entries()) {
|
|
186
190
|
if (!/^\.[A-Za-z0-9]+$/.test(extension))
|
|
187
|
-
invalid(source,
|
|
191
|
+
invalid(source, `${location}.fileInput.extensions[${index}] must be an extension`);
|
|
188
192
|
}
|
|
189
193
|
if (args.filter(arg => arg === '{files}').length !== 1)
|
|
190
|
-
invalid(source,
|
|
194
|
+
invalid(source, `${location}.fileInput requires exactly one {files} argument`);
|
|
191
195
|
command.fileInput = { placeholder: '{files}', extensions };
|
|
192
196
|
}
|
|
193
197
|
else if (args.includes('{files}')) {
|
|
194
|
-
invalid(source,
|
|
198
|
+
invalid(source, `${location} {files} argument requires fileInput`);
|
|
195
199
|
}
|
|
196
200
|
return command;
|
|
197
201
|
}
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoverPresentMarkers = discoverPresentMarkers;
|
|
6
7
|
exports.discoverProjectEvidence = discoverProjectEvidence;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -21,6 +22,26 @@ function safeFile(root, relative) {
|
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Present contained marker files under cwd — same `safeFile` rules pack discovery
|
|
27
|
+
* uses when evaluating applicability allFiles/anyFiles.
|
|
28
|
+
*/
|
|
29
|
+
function discoverPresentMarkers(cwd, markers) {
|
|
30
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
31
|
+
throw new Error('cwd must be a non-empty path');
|
|
32
|
+
if (!Array.isArray(markers))
|
|
33
|
+
throw new Error('markers must be an array');
|
|
34
|
+
let root;
|
|
35
|
+
try {
|
|
36
|
+
root = fs_1.default.realpathSync(cwd);
|
|
37
|
+
if (!fs_1.default.statSync(root).isDirectory())
|
|
38
|
+
throw new Error();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`cwd is not a readable project directory: ${cwd}`);
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(markers.filter((marker) => typeof marker === 'string' && safeFile(root, marker)))].sort();
|
|
44
|
+
}
|
|
24
45
|
function safeParts(parts) {
|
|
25
46
|
return parts.length > 0 && parts.every(part => Boolean(part) && part !== '.' && part !== '..' && !part.includes('/') && !part.includes('\\'));
|
|
26
47
|
}
|
|
@@ -3,7 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isProjectDeclaredSensor = isProjectDeclaredSensor;
|
|
7
|
+
exports.isPackBoundManifestSensor = isPackBoundManifestSensor;
|
|
8
|
+
exports.asPackBoundProjectSensors = asPackBoundProjectSensors;
|
|
6
9
|
exports.legacyCompatibility = legacyCompatibility;
|
|
10
|
+
exports.parseSensorManifestWithIssues = parseSensorManifestWithIssues;
|
|
7
11
|
exports.parseSensorManifest = parseSensorManifest;
|
|
8
12
|
exports.serializeManifestV2 = serializeManifestV2;
|
|
9
13
|
exports.serializeManifestV3 = serializeManifestV3;
|
|
@@ -11,6 +15,28 @@ const contract_1 = require("./contract");
|
|
|
11
15
|
const timeout_1 = require("./timeout");
|
|
12
16
|
const semver_1 = __importDefault(require("semver"));
|
|
13
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
+
/** Registered CLI formatter ids plus exit-code (default for project-declared sensors). */
|
|
19
|
+
const PROJECT_FORMATTERS = new Set([
|
|
20
|
+
'tsc', 'eslint-llm', 'semgrep', 'test', 'mypy', 'ruff', 'shellcheck', 'generic', 'exit-code',
|
|
21
|
+
]);
|
|
22
|
+
function isProjectDeclaredSensor(sensor) {
|
|
23
|
+
return 'source' in sensor && sensor.source === 'project';
|
|
24
|
+
}
|
|
25
|
+
function isPackBoundManifestSensor(sensor) {
|
|
26
|
+
return !isProjectDeclaredSensor(sensor);
|
|
27
|
+
}
|
|
28
|
+
/** Narrow a parsed v3 manifest to the pack-bound shape expected by migrate/materialize. */
|
|
29
|
+
function asPackBoundProjectSensors(manifest) {
|
|
30
|
+
if (typeof manifest.pack !== 'string' || manifest.pack.length === 0 || !manifest.source) {
|
|
31
|
+
throw new Error('project-sensors manifest requires a nonempty pack and source.registry');
|
|
32
|
+
}
|
|
33
|
+
for (const [name, sensor] of Object.entries(manifest.sensors)) {
|
|
34
|
+
if (isProjectDeclaredSensor(sensor)) {
|
|
35
|
+
throw new Error(`sensors.${name} is project-declared; pack-bound operation required`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return manifest;
|
|
39
|
+
}
|
|
14
40
|
function isRecord(value) {
|
|
15
41
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
42
|
}
|
|
@@ -93,6 +119,9 @@ function parseLegacySensor(input, source, location) {
|
|
|
93
119
|
if (typeof input === 'string')
|
|
94
120
|
return { cmd: text(input, source, `${location}.cmd`) };
|
|
95
121
|
const value = record(input, source, location);
|
|
122
|
+
if (value.source === 'project') {
|
|
123
|
+
invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
|
|
124
|
+
}
|
|
96
125
|
fields(value, ['cmd', 'fast', 'enabled', 'timeout', 'changedCmd', 'changedExtensions', 'formatter'], source, location);
|
|
97
126
|
const sensor = {};
|
|
98
127
|
if ('cmd' in value)
|
|
@@ -138,14 +167,96 @@ function parseLegacyManifest(value, source) {
|
|
|
138
167
|
}
|
|
139
168
|
return manifest;
|
|
140
169
|
}
|
|
170
|
+
function parseApplicability(input, source, location) {
|
|
171
|
+
const applicabilityInput = record(input, source, location);
|
|
172
|
+
fields(applicabilityInput, ['allFiles', 'anyFiles', 'kind'], source, location);
|
|
173
|
+
const applicability = {};
|
|
174
|
+
if ('allFiles' in applicabilityInput) {
|
|
175
|
+
applicability.allFiles = stringArray(applicabilityInput.allFiles, source, `${location}.allFiles`, false)
|
|
176
|
+
.map((file, index) => asset(file, source, `${location}.allFiles[${index}]`));
|
|
177
|
+
}
|
|
178
|
+
if ('anyFiles' in applicabilityInput) {
|
|
179
|
+
applicability.anyFiles = stringArray(applicabilityInput.anyFiles, source, `${location}.anyFiles`, false)
|
|
180
|
+
.map((file, index) => asset(file, source, `${location}.anyFiles[${index}]`));
|
|
181
|
+
}
|
|
182
|
+
if ('kind' in applicabilityInput) {
|
|
183
|
+
const kind = text(applicabilityInput.kind, source, `${location}.kind`);
|
|
184
|
+
if (kind !== 'explicit-or-supported-language' && kind !== 'explicit-opt-in') {
|
|
185
|
+
invalid(source, `${location}.kind must be a supported applicability kind`);
|
|
186
|
+
}
|
|
187
|
+
applicability.kind = kind;
|
|
188
|
+
}
|
|
189
|
+
if (Object.keys(applicability).length === 0)
|
|
190
|
+
invalid(source, `${location} must declare a condition`);
|
|
191
|
+
return applicability;
|
|
192
|
+
}
|
|
193
|
+
function parseProjectSensor(input, source, location) {
|
|
194
|
+
const value = record(input, source, location);
|
|
195
|
+
if ('variantId' in value)
|
|
196
|
+
invalid(source, `${location}.variantId is forbidden on source:"project" entries`);
|
|
197
|
+
if ('initializedCompatibility' in value)
|
|
198
|
+
invalid(source, `${location}.initializedCompatibility is forbidden on source:"project" entries`);
|
|
199
|
+
fields(value, ['source', 'enabled', 'command', 'formatter', 'fast', 'timeout', 'assets', 'applicability', 'description'], source, location);
|
|
200
|
+
if (value.source !== 'project')
|
|
201
|
+
invalid(source, `${location}.source must be "project"`);
|
|
202
|
+
if (typeof value.enabled !== 'boolean')
|
|
203
|
+
invalid(source, `${location}.enabled must be a boolean`);
|
|
204
|
+
const sensor = {
|
|
205
|
+
source: 'project',
|
|
206
|
+
enabled: value.enabled,
|
|
207
|
+
command: (0, contract_1.parseStructuredCommand)(value.command, source, `${location}.command`),
|
|
208
|
+
formatter: 'exit-code',
|
|
209
|
+
};
|
|
210
|
+
if ('formatter' in value) {
|
|
211
|
+
const formatter = text(value.formatter, source, `${location}.formatter`);
|
|
212
|
+
if (!PROJECT_FORMATTERS.has(formatter))
|
|
213
|
+
invalid(source, `${location}.formatter is not a registered formatter id`);
|
|
214
|
+
sensor.formatter = formatter;
|
|
215
|
+
}
|
|
216
|
+
if ('assets' in value) {
|
|
217
|
+
sensor.assets = stringArray(value.assets, source, `${location}.assets`, true)
|
|
218
|
+
.map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
|
|
219
|
+
}
|
|
220
|
+
if ('applicability' in value)
|
|
221
|
+
sensor.applicability = parseApplicability(value.applicability, source, `${location}.applicability`);
|
|
222
|
+
if ('description' in value)
|
|
223
|
+
sensor.description = text(value.description, source, `${location}.description`);
|
|
224
|
+
if ('fast' in value) {
|
|
225
|
+
if (typeof value.fast !== 'boolean')
|
|
226
|
+
invalid(source, `${location}.fast must be a boolean`);
|
|
227
|
+
sensor.fast = value.fast;
|
|
228
|
+
}
|
|
229
|
+
if ('timeout' in value) {
|
|
230
|
+
try {
|
|
231
|
+
sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return sensor;
|
|
238
|
+
}
|
|
239
|
+
function parseV3SensorEntry(input, source, location) {
|
|
240
|
+
const value = record(input, source, location);
|
|
241
|
+
if (value.source === 'project')
|
|
242
|
+
return parseProjectSensor(value, source, location);
|
|
243
|
+
const unmarkedCustom = !('variantId' in value) && !('initializedCompatibility' in value) && 'command' in value;
|
|
244
|
+
if (unmarkedCustom) {
|
|
245
|
+
invalid(source, `${location}.source is required for project-declared sensors; add source:"project"`);
|
|
246
|
+
}
|
|
247
|
+
return parseV2Sensor(input, source, location);
|
|
248
|
+
}
|
|
141
249
|
function parseV2Sensor(input, source, location) {
|
|
142
250
|
const value = record(input, source, location);
|
|
251
|
+
if (value.source === 'project') {
|
|
252
|
+
invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
|
|
253
|
+
}
|
|
143
254
|
fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
|
|
144
255
|
if (typeof value.enabled !== 'boolean')
|
|
145
256
|
invalid(source, `${location}.enabled must be a boolean`);
|
|
146
257
|
const sensor = {
|
|
147
258
|
enabled: value.enabled, variantId: id(value.variantId, source, `${location}.variantId`),
|
|
148
|
-
command: (0, contract_1.parseStructuredCommand)(value.command, source),
|
|
259
|
+
command: (0, contract_1.parseStructuredCommand)(value.command, source, `${location}.command`),
|
|
149
260
|
initializedCompatibility: parseCompatibilityEvidence(value.initializedCompatibility, source, `${location}.initializedCompatibility`),
|
|
150
261
|
};
|
|
151
262
|
if (sensor.initializedCompatibility.variantId !== null && sensor.initializedCompatibility.variantId !== sensor.variantId)
|
|
@@ -226,24 +337,28 @@ function parseV2Manifest(value, source) {
|
|
|
226
337
|
}
|
|
227
338
|
return manifest;
|
|
228
339
|
}
|
|
229
|
-
function
|
|
340
|
+
function parseV3ProjectManifestRoot(value, source) {
|
|
230
341
|
fields(value, ['schemaVersion', 'mode', 'pack', 'packSelection', 'source', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
234
|
-
const sensors = {};
|
|
235
|
-
for (const name of Object.keys(sensorsInput))
|
|
236
|
-
sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
|
|
342
|
+
const hasPackKey = 'pack' in value;
|
|
343
|
+
const pack = !hasPackKey ? undefined : value.pack === null ? null : id(value.pack, source, 'pack');
|
|
237
344
|
const manifest = {
|
|
238
345
|
schemaVersion: 3,
|
|
239
346
|
mode: 'project-sensors',
|
|
240
|
-
|
|
241
|
-
source: { registry: id(sourceValue.registry, source, 'source.registry') },
|
|
242
|
-
sensors,
|
|
347
|
+
...(hasPackKey ? { pack } : {}),
|
|
243
348
|
};
|
|
349
|
+
if (typeof pack === 'string' && pack.length > 0) {
|
|
350
|
+
const sourceValue = record(value.source, source, 'source');
|
|
351
|
+
fields(sourceValue, ['registry'], source, 'source');
|
|
352
|
+
manifest.source = { registry: id(sourceValue.registry, source, 'source.registry') };
|
|
353
|
+
}
|
|
354
|
+
else if ('source' in value) {
|
|
355
|
+
invalid(source, 'source requires a nonempty pack id');
|
|
356
|
+
}
|
|
244
357
|
if ('packSelection' in value) {
|
|
245
358
|
if (value.packSelection !== 'explicit')
|
|
246
359
|
invalid(source, 'packSelection must be "explicit" when present');
|
|
360
|
+
if (typeof pack !== 'string' || pack.length === 0)
|
|
361
|
+
invalid(source, 'packSelection requires a nonempty pack id');
|
|
247
362
|
manifest.packSelection = 'explicit';
|
|
248
363
|
}
|
|
249
364
|
if ('packageRoot' in value)
|
|
@@ -255,26 +370,63 @@ function parseV3ProjectManifest(value, source) {
|
|
|
255
370
|
}
|
|
256
371
|
return manifest;
|
|
257
372
|
}
|
|
373
|
+
function parseV3ProjectSensorsMap(sensorsInput, source, soft) {
|
|
374
|
+
const sensors = {};
|
|
375
|
+
const invalidEntries = [];
|
|
376
|
+
for (const name of Object.keys(sensorsInput)) {
|
|
377
|
+
const sensorName = id(name, source, 'sensor id');
|
|
378
|
+
try {
|
|
379
|
+
sensors[sensorName] = parseV3SensorEntry(sensorsInput[name], source, `sensors.${sensorName}`);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
383
|
+
if (!soft)
|
|
384
|
+
throw error instanceof Error ? error : new Error(reason);
|
|
385
|
+
invalidEntries.push({ name: sensorName, reason });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { sensors, invalidEntries };
|
|
389
|
+
}
|
|
390
|
+
function parseV3ProjectManifest(value, source, soft) {
|
|
391
|
+
const root = parseV3ProjectManifestRoot(value, source);
|
|
392
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
393
|
+
const { sensors, invalidEntries } = parseV3ProjectSensorsMap(sensorsInput, source, soft);
|
|
394
|
+
return { pack: { ...root, sensors }, invalidEntries };
|
|
395
|
+
}
|
|
258
396
|
function parseV3Manifest(value, source) {
|
|
259
397
|
if (value.schemaVersion !== 3)
|
|
260
398
|
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
|
|
261
399
|
if (value.mode === 'project-sensors')
|
|
262
|
-
return parseV3ProjectManifest(value, source);
|
|
400
|
+
return parseV3ProjectManifest(value, source, false).pack;
|
|
263
401
|
if (value.mode !== 'native-gate' && value.mode !== 'opt-out')
|
|
264
402
|
invalid(source, 'schemaVersion 3 mode must be "project-sensors", "native-gate", or "opt-out"');
|
|
265
403
|
fields(value, ['schemaVersion', 'mode', 'reason'], source, 'root');
|
|
266
404
|
return { schemaVersion: 3, mode: value.mode, reason: text(value.reason, source, 'reason') };
|
|
267
405
|
}
|
|
268
|
-
|
|
406
|
+
/** Soft per-entry parse for v3 project-sensors; document-level errors still throw. */
|
|
407
|
+
function parseSensorManifestWithIssues(input, source) {
|
|
269
408
|
const value = record(input, source, 'root');
|
|
270
409
|
if (!('schemaVersion' in value))
|
|
271
|
-
return { kind: 'legacy', pack: parseLegacyManifest(value, source) };
|
|
410
|
+
return { kind: 'legacy', pack: parseLegacyManifest(value, source), invalidEntries: [] };
|
|
272
411
|
if (value.schemaVersion === 2)
|
|
273
|
-
return { kind: 'v2', pack: parseV2Manifest(value, source) };
|
|
274
|
-
if (value.schemaVersion === 3)
|
|
275
|
-
|
|
412
|
+
return { kind: 'v2', pack: parseV2Manifest(value, source), invalidEntries: [] };
|
|
413
|
+
if (value.schemaVersion === 3) {
|
|
414
|
+
if (value.mode === 'project-sensors') {
|
|
415
|
+
const parsed = parseV3ProjectManifest(value, source, true);
|
|
416
|
+
return { kind: 'v3', pack: parsed.pack, invalidEntries: parsed.invalidEntries };
|
|
417
|
+
}
|
|
418
|
+
return { kind: 'v3', pack: parseV3Manifest(value, source), invalidEntries: [] };
|
|
419
|
+
}
|
|
276
420
|
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
|
|
277
421
|
}
|
|
422
|
+
function parseSensorManifest(input, source) {
|
|
423
|
+
const withIssues = parseSensorManifestWithIssues(input, source);
|
|
424
|
+
if (withIssues.invalidEntries.length > 0) {
|
|
425
|
+
throw new Error(withIssues.invalidEntries[0].reason);
|
|
426
|
+
}
|
|
427
|
+
const { invalidEntries: _invalidEntries, ...parsed } = withIssues;
|
|
428
|
+
return parsed;
|
|
429
|
+
}
|
|
278
430
|
function serializeManifestV2(input) {
|
|
279
431
|
const parsed = parseSensorManifest(input, 'manifest serialization');
|
|
280
432
|
if (parsed.kind !== 'v2')
|
|
@@ -217,6 +217,23 @@ function priorAssets(projectRoot) {
|
|
|
217
217
|
return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
|
|
218
218
|
return [];
|
|
219
219
|
}
|
|
220
|
+
/** Existing `"source": "project"` entries to keep when regenerating pack sensors (RF-3.1). */
|
|
221
|
+
function priorProjectSensors(projectRoot) {
|
|
222
|
+
const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
223
|
+
try {
|
|
224
|
+
const stat = fs_1.default.lstatSync(manifestPath, { bigint: true });
|
|
225
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
226
|
+
return {};
|
|
227
|
+
// Soft-parse so one invalid sibling does not drop every valid project entry.
|
|
228
|
+
const parsed = (0, manifest_1.parseSensorManifestWithIssues)(JSON.parse((0, safe_file_1.readInspectedBoundedFile)(manifestPath, stat, MAX_ASSET_BYTES, () => new Error('prior manifest unreadable')).toString('utf8')), manifestPath);
|
|
229
|
+
if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
|
|
230
|
+
return {};
|
|
231
|
+
return Object.fromEntries(Object.entries(parsed.pack.sensors).filter(([, sensor]) => (0, manifest_1.isProjectDeclaredSensor)(sensor)));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
220
237
|
/** Materialize a previously selected logical registry source into a portable v3 manifest. */
|
|
221
238
|
function materializePortableSensors(input) {
|
|
222
239
|
if (!input || typeof input !== 'object')
|
|
@@ -237,18 +254,25 @@ function materializePortableSensors(input) {
|
|
|
237
254
|
const parsed = (0, manifest_1.parseSensorManifest)(candidate, 'portable materialized manifest');
|
|
238
255
|
if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
|
|
239
256
|
throw new Error('portable materialized manifest must be v3 project-sensors');
|
|
240
|
-
const
|
|
241
|
-
for (const [name, sensor] of Object.entries(
|
|
242
|
-
if (sensor.policyRef)
|
|
257
|
+
const packBound = (0, manifest_1.asPackBoundProjectSensors)(parsed.pack);
|
|
258
|
+
for (const [name, sensor] of Object.entries(packBound.sensors)) {
|
|
259
|
+
if ((0, manifest_1.isPackBoundManifestSensor)(sensor) && sensor.policyRef)
|
|
243
260
|
(0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
|
|
244
261
|
}
|
|
245
|
-
const
|
|
262
|
+
const priorProject = priorProjectSensors(projectRoot);
|
|
263
|
+
// Project entries win on name collision so regen never drops them (RF-3.1).
|
|
264
|
+
const manifest = {
|
|
265
|
+
...packBound,
|
|
266
|
+
sensors: { ...packBound.sensors, ...priorProject },
|
|
267
|
+
};
|
|
268
|
+
const selected = [...new Set(Object.values(packBound.sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
|
|
246
269
|
// Establish every registry authority and read every selected byte before the
|
|
247
270
|
// first project mutation. A bad later asset must not strand an earlier one.
|
|
248
271
|
const publications = input.configure === false ? [] : selected.map(asset => {
|
|
249
272
|
const selectedSource = selectedRegistryAsset(packRoot, asset);
|
|
250
273
|
return { asset, content: readSelectedRegistryAsset(packRoot, selectedSource, asset) };
|
|
251
274
|
});
|
|
275
|
+
const manifestExists = fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm', 'sensors.json'));
|
|
252
276
|
return (0, safe_file_1.withProjectLease)(projectRoot, () => {
|
|
253
277
|
const configRoot = manifest.packageRoot ? containedPackageRoot(projectRoot, manifest.packageRoot) : projectRoot;
|
|
254
278
|
const prior = priorAssets(projectRoot);
|
|
@@ -273,7 +297,8 @@ function materializePortableSensors(input) {
|
|
|
273
297
|
preserved.push(publication.asset);
|
|
274
298
|
}
|
|
275
299
|
}
|
|
276
|
-
|
|
300
|
+
// Replace when regenerating over an existing manifest so project entries can be re-merged.
|
|
301
|
+
atomicWrite(projectRoot, '.awm/sensors.json', (0, manifest_1.serializeManifestV3)(manifest), !manifestExists);
|
|
277
302
|
}
|
|
278
303
|
catch (error) {
|
|
279
304
|
for (const entry of created.reverse()) {
|
|
@@ -49,10 +49,11 @@ function resolveSensorSource(manifest, deps) {
|
|
|
49
49
|
|| !v3.source || typeof v3.source.registry !== 'string') {
|
|
50
50
|
throw new Error('sensor source resolution requires a valid project-sensors manifest');
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
const registryName = v3.source.registry;
|
|
53
|
+
if (!/^[a-z][a-z0-9-]*$/.test(registryName)) {
|
|
53
54
|
throw new Error('sensor source registry must be a stable lowercase id');
|
|
54
55
|
}
|
|
55
|
-
const matches = registries.filter(entry => entry?.name ===
|
|
56
|
+
const matches = registries.filter(entry => entry?.name === registryName);
|
|
56
57
|
if (matches.length !== 1)
|
|
57
58
|
return unavailable('registry-not-installed');
|
|
58
59
|
const source = (0, pack_source_1.listPackSources)(v3.pack, { registries: matches })[0];
|
|
@@ -225,8 +225,9 @@ function assertCoverageEnvelope(report, renderer) {
|
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
if (staticReport.reason === 'no_reference') {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const packReferenced = isNonBlankString(report.pack) && isNonBlankString(report.registry);
|
|
229
|
+
const onlyProject = report.pack === null && report.registry === null;
|
|
230
|
+
if (report.overall !== 'inconclusive' || staticReport.classes.length !== 0 || (!packReferenced && !onlyProject)) {
|
|
230
231
|
invalidReport(renderer);
|
|
231
232
|
}
|
|
232
233
|
if (report.empirical !== undefined) {
|
|
@@ -303,6 +304,11 @@ function renderCoverageHuman(report) {
|
|
|
303
304
|
return ['Sensor coverage', 'Overall: inconclusive', 'Reason: sensors are not configured', 'Run: awm sensors init', ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
304
305
|
}
|
|
305
306
|
if (report.static.reason === 'no_reference') {
|
|
307
|
+
if (report.pack === null && report.registry === null) {
|
|
308
|
+
return ['Sensor coverage', 'Pack: none', 'Overall: inconclusive',
|
|
309
|
+
'No pack coverage reference (project-declared sensors only)',
|
|
310
|
+
...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
311
|
+
}
|
|
306
312
|
return ['Sensor coverage', `Pack: ${safeHumanText(report.pack ?? 'unknown')}`, `Registry: ${safeHumanText(report.registry ?? 'unknown')}`,
|
|
307
313
|
'Overall: inconclusive', `No coverage reference for pack '${safeHumanText(report.pack ?? 'unknown')}'`, ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
|
|
308
314
|
}
|
|
@@ -97,6 +97,13 @@ function resolveCoverageInputs(cwd) {
|
|
|
97
97
|
if (manifest.kind === 'v3' && manifest.pack.mode !== 'project-sensors')
|
|
98
98
|
return { kind: 'not_configured' };
|
|
99
99
|
const coverageManifest = manifest;
|
|
100
|
+
// Only-project manifests have no pack coverage contract to evaluate (RF-2.9).
|
|
101
|
+
if (coverageManifest.kind === 'v3') {
|
|
102
|
+
const packId = coverageManifest.pack.pack;
|
|
103
|
+
if (typeof packId !== 'string' || packId.length === 0) {
|
|
104
|
+
return { kind: 'no_reference', projectRoot, pack: null, registry: null, manifest: coverageManifest };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
100
107
|
const source = coverageManifest.kind === 'v3'
|
|
101
108
|
? (() => {
|
|
102
109
|
const resolution = (0, source_1.resolveSensorSource)(coverageManifest, { registries: (0, registries_1.listRegistries)() });
|
|
@@ -117,13 +117,15 @@ function readPackDefaults(pack, registryRoot, cwd) {
|
|
|
117
117
|
return sensors;
|
|
118
118
|
}
|
|
119
119
|
function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
120
|
-
const fromPack =
|
|
120
|
+
const fromPack = typeof pack === 'string' && pack.length > 0 && registryRoot
|
|
121
|
+
? readPackDefaults(pack, registryRoot, cwd)
|
|
122
|
+
: null;
|
|
121
123
|
// No registry root, or the pack has no pack.json there → `{}` is the honest floor,
|
|
122
124
|
// not a bug to paper over with CLI-hardcoded defaults. `checkManifest` (preflight)
|
|
123
125
|
// and `computeSensorStatus` both surface a zero-sensor manifest as degraded, with a
|
|
124
126
|
// remedy pointing at the registry — never silently inventing sensors here instead.
|
|
125
127
|
const defaults = fromPack ?? {};
|
|
126
|
-
const existingSensors = existing?.sensors ?? {};
|
|
128
|
+
const existingSensors = (existing?.sensors ?? {});
|
|
127
129
|
// Per-FIELD merge, not whole-sensor-object replacement: if `existingSensors.foo`
|
|
128
130
|
// exists at all, a naive `{ ...defaults, ...existingSensors }` would replace
|
|
129
131
|
// `defaults.foo` wholesale, permanently dropping any field that only lives in the
|
|
@@ -132,11 +134,22 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
|
132
134
|
// field-by-field within each sensor entry lets a user's hand-edited field (e.g. a
|
|
133
135
|
// custom `cmd`) win, while still inheriting any field the existing manifest doesn't
|
|
134
136
|
// specify.
|
|
137
|
+
//
|
|
138
|
+
// Exception — `source: "project"` entries are project-owned: never overlay pack
|
|
139
|
+
// defaults onto them (RF-3.1). Keep the entry byte-stable in meaning.
|
|
135
140
|
const sensorNames = new Set([...Object.keys(defaults), ...Object.keys(existingSensors)]);
|
|
136
141
|
const sensors = {};
|
|
137
142
|
for (const name of sensorNames) {
|
|
143
|
+
const existingEntry = existingSensors[name];
|
|
144
|
+
if (existingEntry && existingEntry.source === 'project') {
|
|
145
|
+
sensors[name] = { ...existingEntry };
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
138
148
|
sensors[name] = { ...defaults[name], ...existingSensors[name] };
|
|
139
149
|
}
|
|
150
|
+
// Only-project write path: omit pack entirely — never invent `generic` (RF-3.2).
|
|
151
|
+
if (typeof pack !== 'string' || pack.length === 0)
|
|
152
|
+
return { sensors };
|
|
140
153
|
return { pack, sensors };
|
|
141
154
|
}
|
|
142
155
|
async function initSensors(opts = {}) {
|
|
@@ -172,10 +172,11 @@ function planV2Migration(input) {
|
|
|
172
172
|
if (validated.kind !== 'v3' || validated.pack.mode !== 'project-sensors') {
|
|
173
173
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
174
174
|
}
|
|
175
|
-
const
|
|
175
|
+
const packBound = (0, manifest_1.asPackBoundProjectSensors)(validated.pack);
|
|
176
|
+
const equivalence = compareV2Semantics(parsed.pack, packBound, source.registry.name);
|
|
176
177
|
if (!allEquivalent(equivalence))
|
|
177
178
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
178
|
-
return { candidate:
|
|
179
|
+
return { candidate: packBound, equivalent: true, equivalence };
|
|
179
180
|
}
|
|
180
181
|
/** Validate the old and new durable contracts, then atomically replace only the manifest. */
|
|
181
182
|
function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
@@ -203,17 +204,18 @@ function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
|
203
204
|
if (before.kind !== 'v2' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
204
205
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
205
206
|
}
|
|
207
|
+
const afterPack = (0, manifest_1.asPackBoundProjectSensors)(after.pack);
|
|
206
208
|
const logicalSource = exactLogicalSource(source, before.pack.pack);
|
|
207
209
|
assertSourceCompatibleWithManifest(logicalSource, before.pack);
|
|
208
|
-
if (
|
|
210
|
+
if (afterPack.source.registry !== logicalSource.registry.name) {
|
|
209
211
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
210
212
|
}
|
|
211
213
|
if (hasPhysicalSensorPath(before.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
212
|
-
|| hasPhysicalSensorPath(
|
|
213
|
-
|| hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(
|
|
214
|
+
|| hasPhysicalSensorPath(afterPack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
215
|
+
|| hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(afterPack.packageRoot, [])) {
|
|
214
216
|
throw new Error('v2 migration candidate contains a physical path');
|
|
215
217
|
}
|
|
216
|
-
if (!allEquivalent(compareV2Semantics(before.pack,
|
|
218
|
+
if (!allEquivalent(compareV2Semantics(before.pack, afterPack, logicalSource.registry.name))) {
|
|
217
219
|
throw new Error('v2 migration candidate semantic mismatch');
|
|
218
220
|
}
|
|
219
221
|
// The native implementation reopens every ancestor without following links,
|
|
@@ -255,10 +257,11 @@ function replaceLegacyManifestWithV3(manifestPath, candidate, source) {
|
|
|
255
257
|
if (before.kind !== 'legacy' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
256
258
|
throw new Error('legacy migration candidate semantic mismatch');
|
|
257
259
|
}
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
const afterPack = (0, manifest_1.asPackBoundProjectSensors)(after.pack);
|
|
261
|
+
const logicalSource = exactLogicalSource({ kind: 'logical', source }, afterPack.pack);
|
|
262
|
+
assertSourceCompatibleWithCandidate(logicalSource, afterPack);
|
|
263
|
+
if (afterPack.source.registry !== logicalSource.registry.name || hasPhysicalSensorPath(afterPack.sensors, [logicalSource.registry.contentRoot])
|
|
264
|
+
|| hasPhysicalSensorPath(afterPack.packageRoot, [])) {
|
|
262
265
|
throw new Error('legacy migration candidate contains an unsafe source or physical path');
|
|
263
266
|
}
|
|
264
267
|
assertManifestUnchanged(manifestPath, inspected);
|