agentic-workflow-manager 9.21.0 → 9.22.1
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/change/index.js +16 -5
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/preflight/checks.js +22 -0
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sensors/compatibility/contract.js +56 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
- package/dist/src/commands/sensors/index.js +10 -1
- package/dist/src/commands/sensors/inputs-digest.js +315 -0
- package/dist/src/commands/sensors/prepare.js +10 -0
- package/dist/src/commands/sensors/result-cache.js +278 -0
- package/dist/src/commands/sensors/result.js +31 -12
- package/dist/src/commands/sensors/run.js +265 -8
- package/dist/src/commands/sensors/status.js +66 -7
- package/dist/src/commands/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/receipt.js +219 -9
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +104 -58
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
- package/dist/tests/commands/sensors/result-cache.test.js +352 -0
- package/dist/tests/commands/sensors/run-cache.test.js +434 -0
- package/dist/tests/commands/sensors/run-slice.test.js +171 -0
- package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
- package/dist/tests/commands/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +279 -8
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +3 -2
- 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
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
|
@@ -23,7 +23,8 @@ const record_io_1 = require("../../core/changes/record-io");
|
|
|
23
23
|
const text_safety_1 = require("../../core/changes/text-safety");
|
|
24
24
|
const types_1 = require("../../core/changes/types");
|
|
25
25
|
const validate_shape_1 = require("../../core/changes/validate-shape");
|
|
26
|
-
|
|
26
|
+
/** RF-4.7: full-gate run; cache may reuse when inputs unchanged — never claim every binary must re-exec. */
|
|
27
|
+
const REMEDY_RECEIPT = 'commit your work, then run full-gate awm sensors run (cache may reuse unchanged sensors)';
|
|
27
28
|
const REMEDY_SPLIT = 'split the change into smaller ones';
|
|
28
29
|
const REMEDY_SENSORS = 'reduce the sensors listed in .awm/sensors.json (the sensor receipt makes the record too large)';
|
|
29
30
|
/** R3.4: the remedy that names the cause of an oversized record: the sensor receipt when it outweighs the module list, else the change itself. */
|
|
@@ -118,7 +119,7 @@ function changeNew(cwd, slug, rawOpts, deps) {
|
|
|
118
119
|
if (!range.ok)
|
|
119
120
|
return (0, command_1.failed)(range.failure);
|
|
120
121
|
const { head } = range;
|
|
121
|
-
const derived = (0, derive_1.deriveFields)(root, range.mergeBase);
|
|
122
|
+
const derived = (0, derive_1.deriveFields)(root, range.mergeBase, head);
|
|
122
123
|
if (!derived.ok)
|
|
123
124
|
return (0, command_1.fail)(derived.diagnostic.code, derived.diagnostic.message);
|
|
124
125
|
const movedAfterDerive = (0, range_1.headMovedFailure)(root, head);
|
|
@@ -254,11 +255,21 @@ function failure(json, code, message, remedy) {
|
|
|
254
255
|
return (0, command_1.fail)(code, message, remedy);
|
|
255
256
|
return render(true, 'invalid', [finish(types_1.RECORD_DIR, [{ code, message: remedy ? `${message}; ${remedy}` : message }])]);
|
|
256
257
|
}
|
|
257
|
-
const deriveOnce = (run) => (run.derived ??= (0, derive_1.deriveFields)(run.root, run.mergeBaseSha));
|
|
258
|
+
const deriveOnce = (run) => (run.derived ??= (0, derive_1.deriveFields)(run.root, run.mergeBaseSha, run.head));
|
|
258
259
|
function lateFiles(run, commit) {
|
|
259
260
|
if (!run.late.has(commit)) {
|
|
260
|
-
const
|
|
261
|
-
|
|
261
|
+
const raw = (0, git_1.changedFiles)(run.root, commit, run.head);
|
|
262
|
+
if (raw === null) {
|
|
263
|
+
run.late.set(commit, null);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// RF-4.5/4.6: when a schema-2 receipt covers every sensor with authored inputs,
|
|
267
|
+
// measured ∪ always-measured ∪ inputs-glob rematch count; otherwise schema-1 rules.
|
|
268
|
+
const receiptRead = (0, receipt_1.readReceipt)(run.root, commit);
|
|
269
|
+
const receipt = receiptRead.ok ? receiptRead.receipt : null;
|
|
270
|
+
const files = (0, receipt_1.filterLateFilesForReceipt)(raw, receipt, types_1.RECORD_DIR);
|
|
271
|
+
run.late.set(commit, { count: files.length, first: files.slice(0, MAX_LATE_PATHS) });
|
|
272
|
+
}
|
|
262
273
|
}
|
|
263
274
|
return run.late.get(commit) ?? null;
|
|
264
275
|
}
|
|
@@ -45,6 +45,7 @@ const doctor_1 = require("./doctor");
|
|
|
45
45
|
const context_1 = require("../core/diagnostics/context");
|
|
46
46
|
const bundles_1 = require("../core/bundles");
|
|
47
47
|
const registries_1 = require("../core/registries");
|
|
48
|
+
const registry_sync_report_1 = require("../core/registry-sync-report");
|
|
48
49
|
const orchestrator_1 = require("../core/init/orchestrator");
|
|
49
50
|
const steps_1 = require("../core/init/steps");
|
|
50
51
|
const failure_1 = require("../core/init/failure");
|
|
@@ -210,10 +211,13 @@ async function runInit(opts = {}) {
|
|
|
210
211
|
(0, registries_1.seedBaselineRegistry)();
|
|
211
212
|
if ((0, registries_1.registriesNeedSync)()) {
|
|
212
213
|
// `syncRegistries()` reports per-registry failures as RESULTS,
|
|
213
|
-
// never as throws (core/registries.ts)
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
(0,
|
|
214
|
+
// never as throws (core/registries.ts). SG-7: init exits non-zero
|
|
215
|
+
// on any sync/clone failure (usable cache is not enough here).
|
|
216
|
+
const syncResults = (await mergedActions.syncCache()) ?? [];
|
|
217
|
+
if ((0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(syncResults)) {
|
|
218
|
+
throw new Error(`registry sync failed — ${(0, registries_1.describeRegistrySyncErrors)((0, registries_1.registrySyncErrors)(syncResults))}`);
|
|
219
|
+
}
|
|
220
|
+
(0, registries_1.assertSyncedRegistriesUsable)(syncResults);
|
|
217
221
|
}
|
|
218
222
|
const bundles = (0, bundles_1.discoverAllBundles)(undefined, reporter);
|
|
219
223
|
const ctx = (0, context_1.gatherContext)({ cwd, bundles, agent });
|
|
@@ -326,6 +326,24 @@ function checkSensorsBaseline(cwd, status) {
|
|
|
326
326
|
+ 'so the gate only chases new problems',
|
|
327
327
|
};
|
|
328
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* RF-3.4 — advisory only: enabled sensors without effective `inputs` are not cacheable.
|
|
331
|
+
* Sourced from `computeSensorStatus().warnings` so status and preflight stay aligned.
|
|
332
|
+
* Omitted when there is nothing to warn about (including opt-out / native-gate).
|
|
333
|
+
*/
|
|
334
|
+
function checkSensorsInputs(status) {
|
|
335
|
+
const warnings = status.warnings ?? [];
|
|
336
|
+
if (warnings.length === 0)
|
|
337
|
+
return null;
|
|
338
|
+
const names = warnings.map(entry => entry.sensor).join(', ');
|
|
339
|
+
return {
|
|
340
|
+
id: 'sensors-inputs',
|
|
341
|
+
ok: false,
|
|
342
|
+
advisory: true,
|
|
343
|
+
detail: `${names} lack effective inputs and are not cacheable`,
|
|
344
|
+
remedy: 'declare inputs on those sensors (project override or pack variant) so results can be reused',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
329
347
|
/**
|
|
330
348
|
* Empirical diagnostics intentionally name only stable execution facts. Sensor output
|
|
331
349
|
* can include source, environment values, or tool-specific unbounded text; preflight
|
|
@@ -528,6 +546,9 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
528
546
|
}
|
|
529
547
|
const sensorStatus = await (0, status_1.computeSensorStatus)(cwd);
|
|
530
548
|
const manifestExists = sensorStatus.mode !== 'missing';
|
|
549
|
+
// RF-3.4: advisory inputs/cacheability warning (status.warnings is empty for
|
|
550
|
+
// opt-out / native-gate / missing, so this stays quiet there).
|
|
551
|
+
const sensorsInputs = checkSensorsInputs(sensorStatus);
|
|
531
552
|
const contextKernel = checkContextKernel(cwd);
|
|
532
553
|
const checks = [
|
|
533
554
|
checkContext(cwd),
|
|
@@ -541,6 +562,7 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
541
562
|
...(sensorStatus.mode !== 'invalid' ? [await checkTools(cwd, sensorStatus), checkPack(cwd, sensorStatus)] : []),
|
|
542
563
|
checkSensorsBaseline(cwd, sensorStatus),
|
|
543
564
|
] : []),
|
|
565
|
+
...(sensorsInputs ? [sensorsInputs] : []),
|
|
544
566
|
// The empirical mode is intentionally opt-in: ordinary preflight remains a
|
|
545
567
|
// quick static inspection and must not dispatch project software.
|
|
546
568
|
...(opts.verifySensors === true ? [await checkSensorExecution(cwd)] : []),
|
|
@@ -8,8 +8,9 @@ exports.addRegistry = addRegistry;
|
|
|
8
8
|
// cli/src/commands/registry/add.ts
|
|
9
9
|
// Logic for `awm registry add`, separated from commander wiring (testable without prompts).
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
-
const simple_git_1 = __importDefault(require("simple-git"));
|
|
12
11
|
const registries_1 = require("../../core/registries");
|
|
12
|
+
const registry_git_1 = require("../../core/registry-git");
|
|
13
|
+
const registry_git_errors_1 = require("../../core/registry-git-errors");
|
|
13
14
|
const discovery_1 = require("../../core/discovery");
|
|
14
15
|
const bundles_1 = require("../../core/bundles");
|
|
15
16
|
const orchestrators_1 = require("../../core/orchestrators");
|
|
@@ -40,12 +41,17 @@ async function addRegistry(remote, nameOverride, reporter) {
|
|
|
40
41
|
return { ok: false, name, error: `Destination already exists on disk: ${dest}` };
|
|
41
42
|
}
|
|
42
43
|
fs_1.default.mkdirSync((0, registries_1.registriesDir)(), { recursive: true });
|
|
44
|
+
(0, registry_git_1.warnIgnoredGitEnvOnce)();
|
|
43
45
|
try {
|
|
44
|
-
await (0,
|
|
46
|
+
await (0, registry_git_1.createRegistryGit)().clone(remote, dest);
|
|
45
47
|
}
|
|
46
48
|
catch (e) {
|
|
47
49
|
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
48
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
name,
|
|
53
|
+
error: (0, registry_git_errors_1.formatClassifiedRegistryFailure)({ name, remote, err: e }),
|
|
54
|
+
};
|
|
49
55
|
}
|
|
50
56
|
if (!(0, registries_1.validateRegistryLayout)(dest)) {
|
|
51
57
|
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
@@ -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.parseSensorInputs = parseSensorInputs;
|
|
6
7
|
exports.parseSemgrepPolicy = parseSemgrepPolicy;
|
|
7
8
|
exports.resolveSemgrepPolicy = resolveSemgrepPolicy;
|
|
8
9
|
exports.parseStructuredCommand = parseStructuredCommand;
|
|
@@ -77,6 +78,48 @@ function assetArray(value, source, location, allowEmpty = false) {
|
|
|
77
78
|
invalid(source, `${location} must be a ${allowEmpty ? '' : 'nonempty '}array`);
|
|
78
79
|
return value.map((item, index) => asset(item, source, `${location}[${index}]`));
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate authored sensor `inputs` globs (RF-1.5).
|
|
83
|
+
* Matching (later digests) uses pinned picomatch with options `{ dot: true, posix: true }`
|
|
84
|
+
* and without `nocase` (paths come from `git ls-files`, already normalized).
|
|
85
|
+
* Supported patterns: `*`, `**`, `?`, character classes. Brace expansion, `!` negation,
|
|
86
|
+
* and parentheses / picomatch extglobs (`@(a|b)`, `+(a)`, `*(a)`, `?(a)`) are rejected
|
|
87
|
+
* here so authors never depend on unsupported syntax.
|
|
88
|
+
*
|
|
89
|
+
* Throws field-scoped Error messages (no "Invalid sensor pack/manifest" prefix) so both
|
|
90
|
+
* pack and project parsers can soft-isolate or wrap without a misleading product noun.
|
|
91
|
+
* `source` is accepted for call-site parity with other parsers; it is unused.
|
|
92
|
+
*/
|
|
93
|
+
function parseSensorInputs(value, _source, location) {
|
|
94
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
95
|
+
throw new Error(`${location} must be a nonempty array`);
|
|
96
|
+
}
|
|
97
|
+
return value.map((item, index) => inputGlob(item, `${location}[${index}]`));
|
|
98
|
+
}
|
|
99
|
+
function inputGlob(value, location) {
|
|
100
|
+
if (typeof value !== 'string' || value.trim().length === 0 || /[\0-\x1f\x7f]/.test(value)) {
|
|
101
|
+
throw new Error(`${location} must be a nonempty single-line string without NUL or control characters`);
|
|
102
|
+
}
|
|
103
|
+
const parsed = value;
|
|
104
|
+
if (parsed.startsWith('/') || /^[A-Za-z]:[\\/]/.test(parsed) || parsed.startsWith('\\\\')) {
|
|
105
|
+
throw new Error(`${location} must be a relative glob (absolute paths are invalid)`);
|
|
106
|
+
}
|
|
107
|
+
if (parsed.includes('\\'))
|
|
108
|
+
throw new Error(`${location} must use POSIX separators`);
|
|
109
|
+
if (parsed.includes('{') || parsed.includes('}')) {
|
|
110
|
+
throw new Error(`${location} must not use brace expansion`);
|
|
111
|
+
}
|
|
112
|
+
if (parsed.includes('!')) {
|
|
113
|
+
throw new Error(`${location} must not use negation (!)`);
|
|
114
|
+
}
|
|
115
|
+
if (parsed.includes('(') || parsed.includes(')')) {
|
|
116
|
+
throw new Error(`${location} must not use parentheses or picomatch extglobs`);
|
|
117
|
+
}
|
|
118
|
+
if (parsed.split('/').some(part => part === '' || part === '.' || part === '..')) {
|
|
119
|
+
throw new Error(`${location} must be a contained relative glob (no empty, ., or .. segments)`);
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
80
123
|
const SEMGREP_POLICY_REF = 'shared/semgrep-policy.json';
|
|
81
124
|
const MAX_POLICY_BYTES = 64 * 1024;
|
|
82
125
|
function contained(root, candidate) {
|
|
@@ -201,7 +244,7 @@ function parseStructuredCommand(input, source, location = 'command') {
|
|
|
201
244
|
}
|
|
202
245
|
function parseVariant(input, source, location) {
|
|
203
246
|
const value = record(input, source, location);
|
|
204
|
-
fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'changedCommand', 'assets', 'formatter', 'probe', 'policyRef'], source, location);
|
|
247
|
+
fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'changedCommand', 'assets', 'formatter', 'probe', 'policyRef', 'inputs'], source, location);
|
|
205
248
|
const certifiedRange = text(value.certifiedRange, source, `${location}.certifiedRange`);
|
|
206
249
|
if (semver_1.default.validRange(certifiedRange) === null)
|
|
207
250
|
invalid(source, `${location}.certifiedRange must be a valid semver range`);
|
|
@@ -239,6 +282,7 @@ function parseVariant(input, source, location) {
|
|
|
239
282
|
const changedCommand = 'changedCommand' in value ? parseStructuredCommand(value.changedCommand, source) : undefined;
|
|
240
283
|
if (changedCommand && !changedCommand.fileInput)
|
|
241
284
|
invalid(source, `${location}.changedCommand must declare fileInput`);
|
|
285
|
+
const inputs = 'inputs' in value ? parseSensorInputs(value.inputs, source, `${location}.inputs`) : undefined;
|
|
242
286
|
return {
|
|
243
287
|
id: id(value.id, source, `${location}.id`),
|
|
244
288
|
priority: value.priority,
|
|
@@ -253,6 +297,7 @@ function parseVariant(input, source, location) {
|
|
|
253
297
|
...(policy ? { policyRef: SEMGREP_POLICY_REF } : {}),
|
|
254
298
|
command: parseStructuredCommand(value.command, source),
|
|
255
299
|
...(changedCommand ? { changedCommand } : {}),
|
|
300
|
+
...(inputs ? { inputs } : {}),
|
|
256
301
|
};
|
|
257
302
|
}
|
|
258
303
|
function parseHardening(input, source) {
|
|
@@ -288,7 +333,7 @@ function assertNoEqualPriorityOverlap(variants) {
|
|
|
288
333
|
}
|
|
289
334
|
function parseSensor(input, source, location, variantIds) {
|
|
290
335
|
const value = record(input, source, location);
|
|
291
|
-
fields(value, ['applicability', 'variants', 'fast', 'timeout'], source, location);
|
|
336
|
+
fields(value, ['applicability', 'variants', 'fast', 'timeout', 'closureOnly'], source, location);
|
|
292
337
|
if (!Array.isArray(value.variants) || value.variants.length === 0)
|
|
293
338
|
invalid(source, `${location}.variants must be a nonempty array`);
|
|
294
339
|
const variants = value.variants.map((variant, index) => parseVariant(variant, source, `${location}.variants[${index}]`));
|
|
@@ -320,6 +365,8 @@ function parseSensor(input, source, location, variantIds) {
|
|
|
320
365
|
invalid(source, `${location}.applicability must declare a condition`);
|
|
321
366
|
if ('fast' in value && typeof value.fast !== 'boolean')
|
|
322
367
|
invalid(source, `${location}.fast must be a boolean`);
|
|
368
|
+
if ('closureOnly' in value && typeof value.closureOnly !== 'boolean')
|
|
369
|
+
invalid(source, `${location}.closureOnly must be a boolean`);
|
|
323
370
|
let timeout;
|
|
324
371
|
if ('timeout' in value) {
|
|
325
372
|
try {
|
|
@@ -329,7 +376,13 @@ function parseSensor(input, source, location, variantIds) {
|
|
|
329
376
|
invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
|
|
330
377
|
}
|
|
331
378
|
}
|
|
332
|
-
return {
|
|
379
|
+
return {
|
|
380
|
+
applicability,
|
|
381
|
+
variants,
|
|
382
|
+
...('fast' in value ? { fast: value.fast } : {}),
|
|
383
|
+
...('closureOnly' in value ? { closureOnly: value.closureOnly } : {}),
|
|
384
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
385
|
+
};
|
|
333
386
|
}
|
|
334
387
|
function legacyCompatibility() {
|
|
335
388
|
return {
|
|
@@ -196,7 +196,7 @@ function parseProjectSensor(input, source, location) {
|
|
|
196
196
|
invalid(source, `${location}.variantId is forbidden on source:"project" entries`);
|
|
197
197
|
if ('initializedCompatibility' in value)
|
|
198
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);
|
|
199
|
+
fields(value, ['source', 'enabled', 'command', 'formatter', 'fast', 'timeout', 'assets', 'applicability', 'description', 'inputs', 'closureOnly'], source, location);
|
|
200
200
|
if (value.source !== 'project')
|
|
201
201
|
invalid(source, `${location}.source must be "project"`);
|
|
202
202
|
if (typeof value.enabled !== 'boolean')
|
|
@@ -226,6 +226,13 @@ function parseProjectSensor(input, source, location) {
|
|
|
226
226
|
invalid(source, `${location}.fast must be a boolean`);
|
|
227
227
|
sensor.fast = value.fast;
|
|
228
228
|
}
|
|
229
|
+
if ('closureOnly' in value) {
|
|
230
|
+
if (typeof value.closureOnly !== 'boolean')
|
|
231
|
+
invalid(source, `${location}.closureOnly must be a boolean`);
|
|
232
|
+
sensor.closureOnly = value.closureOnly;
|
|
233
|
+
}
|
|
234
|
+
if ('inputs' in value)
|
|
235
|
+
sensor.inputs = (0, contract_1.parseSensorInputs)(value.inputs, source, `${location}.inputs`);
|
|
229
236
|
if ('timeout' in value) {
|
|
230
237
|
try {
|
|
231
238
|
sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
@@ -251,7 +258,7 @@ function parseV2Sensor(input, source, location) {
|
|
|
251
258
|
if (value.source === 'project') {
|
|
252
259
|
invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
|
|
253
260
|
}
|
|
254
|
-
fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
|
|
261
|
+
fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility', 'inputs', 'closureOnly'], source, location);
|
|
255
262
|
if (typeof value.enabled !== 'boolean')
|
|
256
263
|
invalid(source, `${location}.enabled must be a boolean`);
|
|
257
264
|
const sensor = {
|
|
@@ -279,6 +286,13 @@ function parseV2Sensor(input, source, location) {
|
|
|
279
286
|
invalid(source, `${location}.fast must be a boolean`);
|
|
280
287
|
sensor.fast = value.fast;
|
|
281
288
|
}
|
|
289
|
+
if ('closureOnly' in value) {
|
|
290
|
+
if (typeof value.closureOnly !== 'boolean')
|
|
291
|
+
invalid(source, `${location}.closureOnly must be a boolean`);
|
|
292
|
+
sensor.closureOnly = value.closureOnly;
|
|
293
|
+
}
|
|
294
|
+
if ('inputs' in value)
|
|
295
|
+
sensor.inputs = (0, contract_1.parseSensorInputs)(value.inputs, source, `${location}.inputs`);
|
|
282
296
|
if ('timeout' in value) {
|
|
283
297
|
try {
|
|
284
298
|
sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
@@ -51,14 +51,18 @@ function registerSensorsCommand(program) {
|
|
|
51
51
|
.option('--fast', 'run fast sensors only (tsc, lint)')
|
|
52
52
|
.option('--slow', 'run slow sensors only (semgrep, mutation)')
|
|
53
53
|
.option('--all', 'run all sensors regardless of speed')
|
|
54
|
+
.option('--slice', 'run every selected sensor except pack/project closureOnly (slice DONE gate)')
|
|
54
55
|
.option('--changed', 'scope sensors that support it to the files changed vs --base')
|
|
55
56
|
.option('--base <ref>', 'comparison point for --changed (default: HEAD, i.e. uncommitted work)')
|
|
57
|
+
.option('--no-cache', 'ignore fingerprint result cache and execute every selected sensor')
|
|
56
58
|
.action(async (opts) => {
|
|
57
59
|
// R2.2b: the receipt belongs to the manifest that runs (the one `runSensors` resolves from this cwd), not to the cwd.
|
|
58
60
|
const pre = (0, receipt_1.isFullSelection)(opts) ? (0, receipt_1.captureReceiptPrecondition)((0, run_1.findManifestDir)(process.cwd()) ?? process.cwd()) : null;
|
|
59
61
|
const output = await (0, run_1.runSensors)({
|
|
60
|
-
fast: opts.fast, slow: opts.slow, all: opts.all,
|
|
62
|
+
fast: opts.fast, slow: opts.slow, all: opts.all, slice: opts.slice,
|
|
61
63
|
changed: opts.changed, base: opts.base,
|
|
64
|
+
// RF-2.11: only this CLI action enables the cache (unless --no-cache).
|
|
65
|
+
useCache: opts.cache !== false,
|
|
62
66
|
});
|
|
63
67
|
// Emit the verdict ALWAYS — an empty `sensors` with overall:'not_certified'
|
|
64
68
|
// must be visible, never a silent exit-0 that reads as "clean".
|
|
@@ -177,6 +181,11 @@ function registerSensorsCommand(program) {
|
|
|
177
181
|
for (const [name, gap] of Object.entries(status.uninitialized ?? {})) {
|
|
178
182
|
console.log(` ${picocolors_1.default.yellow('–')} ${name.padEnd(12)} not initialized: ${gap.state}/${gap.reason}`);
|
|
179
183
|
}
|
|
184
|
+
// RF-3.4: enabled sensors without effective inputs are not cacheable.
|
|
185
|
+
// Non-blocking — same attribution pattern as uninitialized; does not exit.
|
|
186
|
+
for (const warning of status.warnings ?? []) {
|
|
187
|
+
console.log(` ${picocolors_1.default.yellow('⚠')} ${warning.sensor.padEnd(12)} ${warning.detail}`);
|
|
188
|
+
}
|
|
180
189
|
console.log('');
|
|
181
190
|
if (status.overall !== 'READY')
|
|
182
191
|
process.exit(1);
|
|
@@ -0,0 +1,315 @@
|
|
|
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.CACHE_DIGEST_BASELINE_REL = exports.MAX_DIGEST_FILE_BYTES = exports.ABSENT_MARKER = exports.PICOMATCH_DIGEST_OPTIONS = void 0;
|
|
7
|
+
exports.isSafeRepoRel = isSafeRepoRel;
|
|
8
|
+
exports.toSafeRepoRel = toSafeRepoRel;
|
|
9
|
+
exports.joinRepoRel = joinRepoRel;
|
|
10
|
+
exports.buildAlwaysMeasuredPaths = buildAlwaysMeasuredPaths;
|
|
11
|
+
exports.repoPathMatchesInputGlobs = repoPathMatchesInputGlobs;
|
|
12
|
+
exports.computeCacheInputDigest = computeCacheInputDigest;
|
|
13
|
+
/// <reference path="../../types/picomatch-posix.d.ts" />
|
|
14
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const posix_1 = __importDefault(require("picomatch/posix"));
|
|
18
|
+
const git_1 = require("../../core/changes/git");
|
|
19
|
+
/**
|
|
20
|
+
* Picomatch options for sensor input digests (RF-1.5 / RF-1.6).
|
|
21
|
+
* `dot: true` so patterns like `*` match `.env`; `posix: true` enables POSIX
|
|
22
|
+
* character classes. No `nocase` — paths come from `git ls-files` (already
|
|
23
|
+
* normalized). Callers force POSIX path separators via `picomatch/posix`.
|
|
24
|
+
*/
|
|
25
|
+
exports.PICOMATCH_DIGEST_OPTIONS = { dot: true, posix: true };
|
|
26
|
+
/** Explicit marker contributed when a listed path is gone (ENOENT on lstat). */
|
|
27
|
+
exports.ABSENT_MARKER = 'ABSENT';
|
|
28
|
+
/** Hard ceiling on file content hashed into a digest contribution (QA-6). */
|
|
29
|
+
exports.MAX_DIGEST_FILE_BYTES = 1024 * 1024;
|
|
30
|
+
// Baseline is re-applied every run (RF-2.13) and must never enter the cache
|
|
31
|
+
// digest (RF-2.12), even when authored globs like **/*.json would match it.
|
|
32
|
+
/** Repo-root-relative path of the baseline file (never cache-digest measured). */
|
|
33
|
+
exports.CACHE_DIGEST_BASELINE_REL = '.awm/sensors.baseline.json';
|
|
34
|
+
function isExcludedFromCacheDigest(repoRel) {
|
|
35
|
+
const norm = repoRel.split(path_1.default.sep).join('/');
|
|
36
|
+
return norm === exports.CACHE_DIGEST_BASELINE_REL || norm.endsWith('/' + exports.CACHE_DIGEST_BASELINE_REL);
|
|
37
|
+
}
|
|
38
|
+
const LOCKFILE_NAMES = [
|
|
39
|
+
'package-lock.json',
|
|
40
|
+
'pnpm-lock.yaml',
|
|
41
|
+
'yarn.lock',
|
|
42
|
+
'bun.lockb',
|
|
43
|
+
'Pipfile.lock',
|
|
44
|
+
'poetry.lock',
|
|
45
|
+
'uv.lock',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Repo-root-relative path safety (QA-2): nonempty POSIX segments, no `.` / `..`,
|
|
49
|
+
* no absolute / drive / backslash forms.
|
|
50
|
+
*/
|
|
51
|
+
function isSafeRepoRel(repoRel) {
|
|
52
|
+
if (typeof repoRel !== 'string' || repoRel.length === 0 || repoRel !== repoRel.trim())
|
|
53
|
+
return false;
|
|
54
|
+
if (repoRel.includes('\\') || repoRel.startsWith('/') || /^[A-Za-z]:/.test(repoRel))
|
|
55
|
+
return false;
|
|
56
|
+
return repoRel.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
57
|
+
}
|
|
58
|
+
function requireSafeRepoRel(repoRel, label) {
|
|
59
|
+
const norm = repoRel.split(path_1.default.sep).join('/');
|
|
60
|
+
if (!isSafeRepoRel(norm))
|
|
61
|
+
throw new Error(`${label} must be a safe relative path (no ., .., absolute, or escape)`);
|
|
62
|
+
return norm;
|
|
63
|
+
}
|
|
64
|
+
function resolveRealPath(p) {
|
|
65
|
+
const resolved = path_1.default.resolve(p);
|
|
66
|
+
try {
|
|
67
|
+
return fs_1.default.realpathSync.native(resolved);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
try {
|
|
71
|
+
return fs_1.default.realpathSync(resolved);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function posixRel(from, to) {
|
|
79
|
+
return path_1.default.relative(from, to).split(path_1.default.sep).join('/');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Repo-root-relative POSIX path from an absolute (or cwd-relative) path.
|
|
83
|
+
* Realpaths both sides before `path.relative` so Windows drive-letter / casing /
|
|
84
|
+
* separator mismatches (git toplevel vs Node paths) cannot yield an absolute
|
|
85
|
+
* relative string that later fails `isSafeRepoRel`.
|
|
86
|
+
*/
|
|
87
|
+
function toSafeRepoRel(gitRoot, absolutePath, label = 'path') {
|
|
88
|
+
if (typeof gitRoot !== 'string' || gitRoot.trim() === '')
|
|
89
|
+
throw new Error('gitRoot must be a nonempty string');
|
|
90
|
+
if (typeof absolutePath !== 'string' || absolutePath.trim() === '') {
|
|
91
|
+
throw new Error(`${label} must be a nonempty string`);
|
|
92
|
+
}
|
|
93
|
+
const from = resolveRealPath(gitRoot);
|
|
94
|
+
const to = resolveRealPath(absolutePath);
|
|
95
|
+
const rel = posixRel(from, to);
|
|
96
|
+
// Empty means the same path; sensors.json / package roots used here are always nested or equal.
|
|
97
|
+
// Equal packageRoot → '' is allowed only when callers opt in via allowEmpty.
|
|
98
|
+
if (rel === '')
|
|
99
|
+
return '';
|
|
100
|
+
if (path_1.default.win32.isAbsolute(rel) || path_1.default.posix.isAbsolute(rel) || rel.startsWith('..') || /^[A-Za-z]:/.test(rel)) {
|
|
101
|
+
throw new Error(`${label} must resolve under gitRoot`);
|
|
102
|
+
}
|
|
103
|
+
return requireSafeRepoRel(rel, label);
|
|
104
|
+
}
|
|
105
|
+
function staysUnderRoot(rootReal, candidate) {
|
|
106
|
+
const rel = path_1.default.relative(rootReal, candidate);
|
|
107
|
+
return rel === '' || (!rel.startsWith('..') && !path_1.default.isAbsolute(rel));
|
|
108
|
+
}
|
|
109
|
+
function joinRepoRel(packageRootRel, file) {
|
|
110
|
+
if (typeof packageRootRel !== 'string')
|
|
111
|
+
throw new Error('packageRootRel must be a string');
|
|
112
|
+
if (typeof file !== 'string' || file.length === 0)
|
|
113
|
+
throw new Error('file must be a nonempty string');
|
|
114
|
+
const cleaned = requireSafeRepoRel(file.replace(/^\/+/, ''), 'path');
|
|
115
|
+
if (packageRootRel === '')
|
|
116
|
+
return cleaned;
|
|
117
|
+
const rootRel = requireSafeRepoRel(packageRootRel.replace(/\/+$/, ''), 'packageRootRel');
|
|
118
|
+
return `${rootRel}/${cleaned}`;
|
|
119
|
+
}
|
|
120
|
+
/** Build the always-measured path set for the *cache* digest (RF-2.12). Never includes baseline.json. */
|
|
121
|
+
function buildAlwaysMeasuredPaths(input) {
|
|
122
|
+
if (!input || typeof input !== 'object')
|
|
123
|
+
throw new Error('always-measured input must be an object');
|
|
124
|
+
if (typeof input.includePackageJson !== 'boolean')
|
|
125
|
+
throw new Error('includePackageJson must be a boolean');
|
|
126
|
+
if (typeof input.packageRootRel !== 'string')
|
|
127
|
+
throw new Error('packageRootRel must be a string');
|
|
128
|
+
if (!Array.isArray(input.assetRels) || !Array.isArray(input.configFileRels)) {
|
|
129
|
+
throw new Error('assetRels and configFileRels must be arrays');
|
|
130
|
+
}
|
|
131
|
+
const out = new Set();
|
|
132
|
+
out.add(requireSafeRepoRel(input.sensorsJsonRel.split(path_1.default.sep).join('/'), 'sensorsJsonRel'));
|
|
133
|
+
for (const asset of input.assetRels) {
|
|
134
|
+
if (typeof asset !== 'string' || asset.length === 0)
|
|
135
|
+
throw new Error('asset path must be a nonempty string');
|
|
136
|
+
out.add(joinRepoRel(input.packageRootRel, asset));
|
|
137
|
+
}
|
|
138
|
+
for (const config of input.configFileRels) {
|
|
139
|
+
if (typeof config !== 'string' || config.length === 0)
|
|
140
|
+
throw new Error('configFile path must be a nonempty string');
|
|
141
|
+
out.add(joinRepoRel(input.packageRootRel, config));
|
|
142
|
+
}
|
|
143
|
+
if (input.includePackageJson) {
|
|
144
|
+
out.add(joinRepoRel(input.packageRootRel, 'package.json'));
|
|
145
|
+
for (const lock of LOCKFILE_NAMES)
|
|
146
|
+
out.add(joinRepoRel(input.packageRootRel, lock));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
for (const lock of LOCKFILE_NAMES)
|
|
150
|
+
out.add(joinRepoRel(input.packageRootRel, lock));
|
|
151
|
+
}
|
|
152
|
+
return [...out].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
153
|
+
}
|
|
154
|
+
function listGitFiles(gitRoot) {
|
|
155
|
+
const out = (0, git_1.runGit)(gitRoot, ['ls-files', '-co', '--exclude-standard', '-z']);
|
|
156
|
+
if (out === null)
|
|
157
|
+
return null;
|
|
158
|
+
return out.split('\0').filter((p) => p.length > 0);
|
|
159
|
+
}
|
|
160
|
+
function matchGlobs(repoRel, packageRootRel, matchers) {
|
|
161
|
+
let packageRel = repoRel;
|
|
162
|
+
if (packageRootRel) {
|
|
163
|
+
const prefix = packageRootRel.endsWith('/') ? packageRootRel : `${packageRootRel}/`;
|
|
164
|
+
if (repoRel !== packageRootRel && !repoRel.startsWith(prefix))
|
|
165
|
+
return false;
|
|
166
|
+
packageRel = repoRel === packageRootRel ? '' : repoRel.slice(prefix.length);
|
|
167
|
+
}
|
|
168
|
+
if (packageRel.length === 0)
|
|
169
|
+
return false;
|
|
170
|
+
return matchers.some((m) => m(packageRel));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Whether a repo-root POSIX path matches authored sensor `inputs` globs under
|
|
174
|
+
* `packageRootRel` (RF-1.5 picomatch options). Used by receipt late-path validate
|
|
175
|
+
* so new files matching inputs count as measured (RF-4.5), not only the frozen list.
|
|
176
|
+
*/
|
|
177
|
+
function repoPathMatchesInputGlobs(repoRel, packageRootRel, inputGlobs) {
|
|
178
|
+
if (typeof repoRel !== 'string' || typeof packageRootRel !== 'string')
|
|
179
|
+
return false;
|
|
180
|
+
if (!Array.isArray(inputGlobs) || inputGlobs.some((g) => typeof g !== 'string'))
|
|
181
|
+
return false;
|
|
182
|
+
const norm = repoRel.split(path_1.default.sep).join('/');
|
|
183
|
+
if (!isSafeRepoRel(norm))
|
|
184
|
+
return false;
|
|
185
|
+
if (packageRootRel !== '' && !isSafeRepoRel(packageRootRel))
|
|
186
|
+
return false;
|
|
187
|
+
const matchers = inputGlobs.map((glob) => (0, posix_1.default)(glob, { ...exports.PICOMATCH_DIGEST_OPTIONS }));
|
|
188
|
+
return matchGlobs(norm, packageRootRel, matchers);
|
|
189
|
+
}
|
|
190
|
+
function absentContribution(repoRel, reason) {
|
|
191
|
+
return {
|
|
192
|
+
path: repoRel,
|
|
193
|
+
kind: 'absent',
|
|
194
|
+
marker: exports.ABSENT_MARKER,
|
|
195
|
+
hash: crypto_1.default.createHash('sha256').update(`ABSENT:${reason}:${repoRel}`).digest('hex'),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Hash one matched path without following intermediate symlinks (QA-1) and
|
|
200
|
+
* without reading files that escape gitRoot or exceed the size cap (QA-2/QA-6).
|
|
201
|
+
*/
|
|
202
|
+
function hashContribution(gitRootReal, repoRel) {
|
|
203
|
+
if (!isSafeRepoRel(repoRel))
|
|
204
|
+
return absentContribution(repoRel, 'unsafe-rel');
|
|
205
|
+
const segments = repoRel.split('/');
|
|
206
|
+
let current = gitRootReal;
|
|
207
|
+
for (let i = 0; i < segments.length; i++) {
|
|
208
|
+
current = path_1.default.join(current, segments[i]);
|
|
209
|
+
if (!staysUnderRoot(gitRootReal, current))
|
|
210
|
+
return absentContribution(repoRel, 'escape');
|
|
211
|
+
let st;
|
|
212
|
+
try {
|
|
213
|
+
st = fs_1.default.lstatSync(current);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error.code === 'ENOENT')
|
|
217
|
+
return absentContribution(repoRel, 'enoent');
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
const isLast = i === segments.length - 1;
|
|
221
|
+
if (st.isSymbolicLink()) {
|
|
222
|
+
// Intermediate symlink → fail closed (never read through redirected parents).
|
|
223
|
+
if (!isLast)
|
|
224
|
+
return absentContribution(repoRel, 'intermediate-symlink');
|
|
225
|
+
const target = fs_1.default.readlinkSync(current);
|
|
226
|
+
return {
|
|
227
|
+
path: repoRel,
|
|
228
|
+
kind: 'symlink',
|
|
229
|
+
hash: crypto_1.default.createHash('sha256').update(`symlink:${target}`).digest('hex'),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (!isLast) {
|
|
233
|
+
if (!st.isDirectory())
|
|
234
|
+
return absentContribution(repoRel, 'not-dir');
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!st.isFile())
|
|
238
|
+
return absentContribution(repoRel, 'not-file');
|
|
239
|
+
if (st.size > exports.MAX_DIGEST_FILE_BYTES)
|
|
240
|
+
return absentContribution(repoRel, 'oversize');
|
|
241
|
+
const content = fs_1.default.readFileSync(current);
|
|
242
|
+
return {
|
|
243
|
+
path: repoRel,
|
|
244
|
+
kind: 'file',
|
|
245
|
+
hash: crypto_1.default.createHash('sha256').update(content).digest('hex'),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return absentContribution(repoRel, 'empty');
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Compute the cache input digest (RF-2.1 / RF-2.12): git enumeration, picomatch
|
|
252
|
+
* filter, symlink readlink / ABSENT / file content hashing. Never follows links.
|
|
253
|
+
*/
|
|
254
|
+
function computeCacheInputDigest(opts) {
|
|
255
|
+
if (!opts || typeof opts !== 'object')
|
|
256
|
+
throw new Error('digest options must be an object');
|
|
257
|
+
if (typeof opts.gitRoot !== 'string' || opts.gitRoot.trim() === '')
|
|
258
|
+
throw new Error('gitRoot must be a nonempty string');
|
|
259
|
+
if (typeof opts.packageRoot !== 'string' || opts.packageRoot.trim() === '')
|
|
260
|
+
throw new Error('packageRoot must be a nonempty string');
|
|
261
|
+
if (!Array.isArray(opts.inputGlobs) || opts.inputGlobs.some((g) => typeof g !== 'string')) {
|
|
262
|
+
throw new Error('inputGlobs must be an array of strings');
|
|
263
|
+
}
|
|
264
|
+
if (!Array.isArray(opts.alwaysMeasuredRepoRels) || opts.alwaysMeasuredRepoRels.some((p) => typeof p !== 'string')) {
|
|
265
|
+
throw new Error('alwaysMeasuredRepoRels must be an array of strings');
|
|
266
|
+
}
|
|
267
|
+
// Prefer realpathSync.native (same as toSafeRepoRel / run.ts). Mixing native vs
|
|
268
|
+
// non-native on Windows yields drive-absolute path.relative results and false
|
|
269
|
+
// "packageRoot must be inside gitRoot" after the sensorsJsonRel fix.
|
|
270
|
+
let gitRootReal;
|
|
271
|
+
try {
|
|
272
|
+
gitRootReal = fs_1.default.realpathSync.native(path_1.default.resolve(opts.gitRoot));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
try {
|
|
276
|
+
gitRootReal = fs_1.default.realpathSync(path_1.default.resolve(opts.gitRoot));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return { ok: false, reason: 'git-failed' };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
let packageRootRel;
|
|
283
|
+
try {
|
|
284
|
+
packageRootRel = toSafeRepoRel(gitRootReal, opts.packageRoot, 'packageRoot');
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
throw new Error('packageRoot must be inside gitRoot');
|
|
288
|
+
}
|
|
289
|
+
const listed = listGitFiles(gitRootReal);
|
|
290
|
+
if (listed === null)
|
|
291
|
+
return { ok: false, reason: 'git-failed' };
|
|
292
|
+
const matchers = opts.inputGlobs.map((glob) => (0, posix_1.default)(glob, { ...exports.PICOMATCH_DIGEST_OPTIONS }));
|
|
293
|
+
const matched = new Set();
|
|
294
|
+
for (const repoRel of listed) {
|
|
295
|
+
const norm = repoRel.split(path_1.default.sep).join('/');
|
|
296
|
+
if (!isSafeRepoRel(norm) || isExcludedFromCacheDigest(norm))
|
|
297
|
+
continue;
|
|
298
|
+
if (matchGlobs(norm, packageRootRel === '' ? '' : packageRootRel, matchers))
|
|
299
|
+
matched.add(norm);
|
|
300
|
+
}
|
|
301
|
+
for (const always of opts.alwaysMeasuredRepoRels) {
|
|
302
|
+
const norm = always.split(path_1.default.sep).join('/');
|
|
303
|
+
if (!isSafeRepoRel(norm)) {
|
|
304
|
+
throw new Error(`alwaysMeasuredRepoRels entry must be a safe relative path (no ., .., absolute, or escape): ${norm}`);
|
|
305
|
+
}
|
|
306
|
+
if (isExcludedFromCacheDigest(norm))
|
|
307
|
+
continue;
|
|
308
|
+
matched.add(norm);
|
|
309
|
+
}
|
|
310
|
+
const matchedPaths = [...matched].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
311
|
+
const contributions = matchedPaths.map((p) => hashContribution(gitRootReal, p));
|
|
312
|
+
const canonical = contributions.map((c) => `${c.path}\0${c.kind}\0${c.hash}`).join('\n');
|
|
313
|
+
const digest = `sha256:${crypto_1.default.createHash('sha256').update(canonical).digest('hex')}`;
|
|
314
|
+
return { ok: true, digest, matchedPaths, contributions };
|
|
315
|
+
}
|