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.
- package/dist/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +382 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +92 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +244 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +95 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
- package/dist/tests/commands/sensors/exec.test.js +115 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +204 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -7,6 +7,8 @@ exports.computeSensorStatus = computeSensorStatus;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const paths_1 = require("../../core/paths");
|
|
10
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
11
|
+
const live_1 = require("./compatibility/live");
|
|
10
12
|
/** First non-flag token after `npx` — the tool the command actually runs. */
|
|
11
13
|
function npxTool(parts) {
|
|
12
14
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -53,16 +55,57 @@ function checkCmd(cmd, cwd) {
|
|
|
53
55
|
}
|
|
54
56
|
return configCheck(parts, cwd) ?? { ok: true, detail: bin };
|
|
55
57
|
}
|
|
56
|
-
function computeSensorStatus(cwd = process.cwd()) {
|
|
58
|
+
async function computeSensorStatus(cwd = process.cwd()) {
|
|
57
59
|
const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
|
|
58
60
|
if (!fs_1.default.existsSync(manifestPath)) {
|
|
59
61
|
return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
|
|
60
62
|
}
|
|
61
63
|
let manifest;
|
|
62
64
|
try {
|
|
63
|
-
|
|
65
|
+
const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
66
|
+
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
67
|
+
if (parsed.kind === 'v2') {
|
|
68
|
+
const checks = {};
|
|
69
|
+
let live;
|
|
70
|
+
try {
|
|
71
|
+
live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot, { packSelection: parsed.pack.packSelection });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const detail = `compatibility revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
75
|
+
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
76
|
+
checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' } : { ok: false, detail };
|
|
77
|
+
}
|
|
78
|
+
return { overall: 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
79
|
+
}
|
|
80
|
+
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
81
|
+
const state = live.sensors[name];
|
|
82
|
+
checks[name] = sensor.enabled === false
|
|
83
|
+
? { ok: true, detail: 'disabled' }
|
|
84
|
+
: !state
|
|
85
|
+
? { ok: false, detail: 'variant-drift: sensor no longer exists in the live pack; run `awm sensors init`' }
|
|
86
|
+
: state.variantId !== sensor.variantId
|
|
87
|
+
? { ok: false, detail: `variant-drift: manifest ${sensor.variantId}, live ${state.variantId ?? 'none'}; run \`awm sensors init\`` }
|
|
88
|
+
: state.state === 'certified'
|
|
89
|
+
? { ok: true, detail: `certified (${state.variantId})` }
|
|
90
|
+
: state.state === 'not-applicable'
|
|
91
|
+
? { ok: true, detail: 'not applicable' }
|
|
92
|
+
: { ok: false, detail: `${state.state}: ${state.reason}` };
|
|
93
|
+
}
|
|
94
|
+
return { overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'HEALTHY' : 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
95
|
+
}
|
|
96
|
+
manifest = parsed.pack;
|
|
64
97
|
}
|
|
65
98
|
catch {
|
|
99
|
+
// Preserve the historic diagnostic path for a hand-edited legacy manifest:
|
|
100
|
+
// malformed v2 is fail-closed, while a legacy `sensors:null` is surfaced as
|
|
101
|
+
// degraded rather than crashing the status command.
|
|
102
|
+
try {
|
|
103
|
+
const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
104
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw) && !('schemaVersion' in raw) && typeof raw.pack === 'string') {
|
|
105
|
+
return { overall: 'DEGRADED', pack: raw.pack, checks: {} };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { /* malformed JSON is not configured */ }
|
|
66
109
|
return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
|
|
67
110
|
}
|
|
68
111
|
const checks = {};
|
|
@@ -84,6 +127,7 @@ function computeSensorStatus(cwd = process.cwd()) {
|
|
|
84
127
|
if (Object.keys(manifest.sensors ?? {}).length === 0) {
|
|
85
128
|
return { overall: 'DEGRADED', pack: manifest.pack, checks };
|
|
86
129
|
}
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
// A legacy manifest may be operational (the checks are still useful), but it
|
|
131
|
+
// has no versioned/structured contract and must never present as certified.
|
|
132
|
+
return { overall: 'DEGRADED', pack: manifest.pack, checks };
|
|
89
133
|
}
|
|
@@ -77,6 +77,13 @@ async function runSyncCore(options, deps = {}) {
|
|
|
77
77
|
if (r.action === 'error')
|
|
78
78
|
console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
|
|
79
79
|
}
|
|
80
|
+
try {
|
|
81
|
+
(0, registries_1.assertSyncedRegistriesUsable)(syncResults);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error(picocolors_1.default.red(e.message));
|
|
85
|
+
return { code: 1, selectedAgents };
|
|
86
|
+
}
|
|
80
87
|
// Gate minCliVersion (WS-4) before pins (contract gates first — CONSTITUTION).
|
|
81
88
|
try {
|
|
82
89
|
(0, registries_1.assertRegistryGates)(d.verifyMinCliVersions());
|
package/dist/src/core/bundles.js
CHANGED
|
@@ -16,6 +16,41 @@ const registries_1 = require("./registries");
|
|
|
16
16
|
function catalogPath(contentDir) {
|
|
17
17
|
return path_1.default.join(contentDir, 'catalog.json');
|
|
18
18
|
}
|
|
19
|
+
function bundleManifestPath(contentDir, source) {
|
|
20
|
+
const root = path_1.default.resolve(contentDir);
|
|
21
|
+
const manifest = path_1.default.resolve(root, source, 'bundle.json');
|
|
22
|
+
const relative = path_1.default.relative(root, manifest);
|
|
23
|
+
if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
|
|
24
|
+
throw new Error(`Invalid bundle source "${source}" in ${catalogPath(contentDir)}: it must remain inside the registry content root.`);
|
|
25
|
+
}
|
|
26
|
+
let rootStat;
|
|
27
|
+
try {
|
|
28
|
+
rootStat = fs_1.default.lstatSync(root);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`Cannot inspect registry content root ${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
34
|
+
throw new Error(`Registry content root ${root} must be a regular directory`);
|
|
35
|
+
}
|
|
36
|
+
let current = root;
|
|
37
|
+
for (const segment of relative.split(path_1.default.sep)) {
|
|
38
|
+
current = path_1.default.join(current, segment);
|
|
39
|
+
let stat;
|
|
40
|
+
try {
|
|
41
|
+
stat = fs_1.default.lstatSync(current);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === 'ENOENT')
|
|
45
|
+
break;
|
|
46
|
+
throw new Error(`Cannot inspect registry bundle path ${current}: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
48
|
+
if (stat.isSymbolicLink()) {
|
|
49
|
+
throw new Error(`Registry bundle path ${current} must not be a symbolic link`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
19
54
|
/** Lee `catalog.json` de un registry.
|
|
20
55
|
*
|
|
21
56
|
* El contenido de un registry es INPUT NO CONFIABLE (un registry de equipo,
|
|
@@ -28,7 +63,7 @@ function catalogPath(contentDir) {
|
|
|
28
63
|
* dos eran los que faltaban. */
|
|
29
64
|
function readCatalog(contentDir) {
|
|
30
65
|
const file = catalogPath(contentDir);
|
|
31
|
-
if (!
|
|
66
|
+
if (!(0, registries_1.assertRegularRegistryFile)(file))
|
|
32
67
|
return [];
|
|
33
68
|
let parsed;
|
|
34
69
|
try {
|
|
@@ -74,8 +109,8 @@ function discoverBundles(contentDir) {
|
|
|
74
109
|
const entries = readCatalog(contentDir);
|
|
75
110
|
const bundles = [];
|
|
76
111
|
for (const entry of entries) {
|
|
77
|
-
const manifestPath =
|
|
78
|
-
if (!
|
|
112
|
+
const manifestPath = bundleManifestPath(contentDir, entry.source);
|
|
113
|
+
if (!(0, registries_1.assertRegularRegistryFile)(manifestPath))
|
|
79
114
|
continue;
|
|
80
115
|
let raw;
|
|
81
116
|
try {
|
|
@@ -65,8 +65,8 @@ exports.defaultActions = {
|
|
|
65
65
|
method: o.method,
|
|
66
66
|
contentDir: o.contentDir,
|
|
67
67
|
}),
|
|
68
|
-
initSensors: (o) => {
|
|
69
|
-
const result = (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
|
|
68
|
+
initSensors: async (o) => {
|
|
69
|
+
const result = await (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
|
|
70
70
|
// `unavailablePack`/`manifest` viajan a proposito: `stepSensors` los reporta.
|
|
71
71
|
// Este wrapper proyectaba solo `detection`, asi que cualquier hallazgo nuevo de
|
|
72
72
|
// initSensors moria aca en silencio antes de llegar al step que lo muestra.
|
|
@@ -216,6 +216,9 @@ function stepDevCore(d) {
|
|
|
216
216
|
// `awm doctor` decia `healthy`. Su propio `globalUnsupportedReason` ya dice
|
|
217
217
|
// que hay que instalar por proyecto, asi que eso es lo que se hace.
|
|
218
218
|
const localOnly = (0, providers_1.providerFor)(d.agent).skill.global === null;
|
|
219
|
+
if (localOnly && d.machineOnly) {
|
|
220
|
+
return ok('machine.devCore', 'machine', 'skipped', `${(0, providers_1.providerFor)(d.agent).label} provides skills at project scope — deferred until project initialization`);
|
|
221
|
+
}
|
|
219
222
|
for (const bundleName of toInstall) {
|
|
220
223
|
d.actions.installBundle({
|
|
221
224
|
bundleName,
|
|
@@ -316,13 +319,13 @@ function stepActivation(d) {
|
|
|
316
319
|
return ok('project.activation', 'project', 'applied');
|
|
317
320
|
}
|
|
318
321
|
/** Step 7 – Initialize the sensor manifest if absent. */
|
|
319
|
-
function stepSensors(d) {
|
|
322
|
+
async function stepSensors(d) {
|
|
320
323
|
const proj = d.ctx.project;
|
|
321
324
|
if (!proj)
|
|
322
325
|
return ok('project.sensors', 'project', 'skipped', 'no project');
|
|
323
326
|
if (proj.sensors.present)
|
|
324
327
|
return ok('project.sensors', 'project', 'skipped');
|
|
325
|
-
const res = d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
328
|
+
const res = await d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
326
329
|
// A registry without the detected pack yields a fallback (or empty) manifest. The
|
|
327
330
|
// step still succeeded — a manifest was written — but reporting a bare 'applied'
|
|
328
331
|
// would let `awm init` hand back a quality gate that measures less than the
|
|
@@ -0,0 +1,228 @@
|
|
|
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.DEFAULT_LEDGER_SCAN_LIMITS = void 0;
|
|
7
|
+
exports.scanProjectLedgers = scanProjectLedgers;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
exports.DEFAULT_LEDGER_SCAN_LIMITS = {
|
|
12
|
+
maxFiles: 256,
|
|
13
|
+
maxFileBytes: 4 * 1024 * 1024,
|
|
14
|
+
maxEntries: 20_000,
|
|
15
|
+
maxLineBytes: 64 * 1024,
|
|
16
|
+
maxJsonDepth: 16,
|
|
17
|
+
maxRefsPerClass: 128,
|
|
18
|
+
};
|
|
19
|
+
function isWithin(root, candidate) {
|
|
20
|
+
const relative = path_1.default.relative(root, candidate);
|
|
21
|
+
return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
|
|
22
|
+
}
|
|
23
|
+
function assertDirectoryInside(root, directory) {
|
|
24
|
+
if (!fs_1.default.existsSync(directory))
|
|
25
|
+
return false;
|
|
26
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
27
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
28
|
+
throw new Error(`ledger directory is unsafe: ${directory}`);
|
|
29
|
+
return isWithin(root, fs_1.default.realpathSync(directory));
|
|
30
|
+
}
|
|
31
|
+
function isJsonDepthWithinLimit(raw, maxDepth) {
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let quoted = false;
|
|
34
|
+
let escaped = false;
|
|
35
|
+
for (const char of raw) {
|
|
36
|
+
if (quoted) {
|
|
37
|
+
if (escaped)
|
|
38
|
+
escaped = false;
|
|
39
|
+
else if (char === '\\')
|
|
40
|
+
escaped = true;
|
|
41
|
+
else if (char === '"')
|
|
42
|
+
quoted = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '"')
|
|
46
|
+
quoted = true;
|
|
47
|
+
else if (char === '{' || char === '[') {
|
|
48
|
+
depth += 1;
|
|
49
|
+
if (depth > maxDepth)
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
else if (char === '}' || char === ']')
|
|
53
|
+
depth -= 1;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function boundedLimits(overrides) {
|
|
58
|
+
if (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides)) {
|
|
59
|
+
throw new Error('ledger scan limits must be an object');
|
|
60
|
+
}
|
|
61
|
+
const limits = { ...exports.DEFAULT_LEDGER_SCAN_LIMITS, ...overrides };
|
|
62
|
+
for (const value of Object.values(limits)) {
|
|
63
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
64
|
+
throw new Error('ledger scan limits must be positive safe integers');
|
|
65
|
+
}
|
|
66
|
+
return limits;
|
|
67
|
+
}
|
|
68
|
+
function relativeSource(root, target, line) {
|
|
69
|
+
return `${relativePath(root, target)}:${line}`;
|
|
70
|
+
}
|
|
71
|
+
function relativePath(root, target) {
|
|
72
|
+
return path_1.default.relative(root, target).split(path_1.default.sep).join('/');
|
|
73
|
+
}
|
|
74
|
+
function compareCandidates(a, b) {
|
|
75
|
+
return a.sourcePath < b.sourcePath ? -1 : a.sourcePath > b.sourcePath ? 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Reads one global bounded window across the direct ledger directories, then
|
|
79
|
+
* sorts only the retained candidates. This deliberately does not claim to
|
|
80
|
+
* find a globally lexical prefix in an unbounded directory: the extra
|
|
81
|
+
* candidate is a truncation witness, and no later candidate is opened or
|
|
82
|
+
* retained from either active or archive storage.
|
|
83
|
+
*/
|
|
84
|
+
function collectBoundedCandidates(root, directories, maxFiles) {
|
|
85
|
+
const candidates = [];
|
|
86
|
+
const capacity = maxFiles + 1;
|
|
87
|
+
let truncated = false;
|
|
88
|
+
for (const { directory, archive } of directories) {
|
|
89
|
+
if (candidates.length >= capacity)
|
|
90
|
+
break;
|
|
91
|
+
const handle = fs_1.default.opendirSync(directory);
|
|
92
|
+
try {
|
|
93
|
+
let item;
|
|
94
|
+
while ((item = handle.readSync()) !== null) {
|
|
95
|
+
if (!item.name.endsWith('.jsonl'))
|
|
96
|
+
continue;
|
|
97
|
+
const target = path_1.default.join(directory, item.name);
|
|
98
|
+
candidates.push({ isArchive: archive, target, sourcePath: relativePath(root, target) });
|
|
99
|
+
if (candidates.length === capacity) {
|
|
100
|
+
truncated = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
handle.closeSync();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
candidates.sort(compareCandidates);
|
|
110
|
+
return { candidates, truncated: truncated || candidates.length > maxFiles };
|
|
111
|
+
}
|
|
112
|
+
/** Reads only direct .jsonl children of active/archive ledger roots; never follows links. */
|
|
113
|
+
function scanProjectLedgers(projectRoot, overrides = {}) {
|
|
114
|
+
if (typeof projectRoot !== 'string' || !path_1.default.isAbsolute(projectRoot))
|
|
115
|
+
throw new Error('projectRoot must be an absolute path');
|
|
116
|
+
const limits = boundedLimits(overrides);
|
|
117
|
+
const root = fs_1.default.realpathSync(projectRoot);
|
|
118
|
+
const ledgerRoot = path_1.default.join(root, '.awm', 'ledger');
|
|
119
|
+
const sources = {
|
|
120
|
+
activeFiles: 0, archivedFiles: 0, validEntries: 0, validFindings: 0, skippedFindings: 0, skippedByReason: {},
|
|
121
|
+
};
|
|
122
|
+
const entries = [];
|
|
123
|
+
const skip = (reason) => {
|
|
124
|
+
sources.skippedFindings += 1;
|
|
125
|
+
sources.skippedByReason[reason] = (sources.skippedByReason[reason] ?? 0) + 1;
|
|
126
|
+
};
|
|
127
|
+
if (!fs_1.default.existsSync(ledgerRoot))
|
|
128
|
+
return { entries, sources, omittedEvidenceRefs: 0 };
|
|
129
|
+
if (!assertDirectoryInside(root, path_1.default.join(root, '.awm')) || !assertDirectoryInside(root, ledgerRoot)) {
|
|
130
|
+
throw new Error('ledger directory escapes project root');
|
|
131
|
+
}
|
|
132
|
+
const directories = [{ directory: ledgerRoot, archive: false }];
|
|
133
|
+
const archive = path_1.default.join(ledgerRoot, 'archive');
|
|
134
|
+
if (fs_1.default.existsSync(archive)) {
|
|
135
|
+
if (!assertDirectoryInside(root, archive))
|
|
136
|
+
throw new Error('ledger archive directory escapes project root');
|
|
137
|
+
directories.push({ directory: archive, archive: true });
|
|
138
|
+
}
|
|
139
|
+
let filesSeen = 0;
|
|
140
|
+
let linesSeen = 0;
|
|
141
|
+
let entryLimitReached = false;
|
|
142
|
+
let omittedEvidenceRefs = 0;
|
|
143
|
+
const evidenceRefsByClass = new Map();
|
|
144
|
+
const collected = collectBoundedCandidates(root, directories, limits.maxFiles);
|
|
145
|
+
for (const { isArchive, target } of collected.candidates.slice(0, limits.maxFiles)) {
|
|
146
|
+
if (entryLimitReached)
|
|
147
|
+
break;
|
|
148
|
+
filesSeen += 1;
|
|
149
|
+
const stat = fs_1.default.lstatSync(target);
|
|
150
|
+
if (stat.isSymbolicLink()) {
|
|
151
|
+
skip('symlink-entry');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!stat.isFile()) {
|
|
155
|
+
skip('nonregular-entry');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const real = fs_1.default.realpathSync(target);
|
|
159
|
+
if (!isWithin(root, real)) {
|
|
160
|
+
skip('path-escape');
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (stat.size > limits.maxFileBytes) {
|
|
164
|
+
skip('file-too-large');
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (isArchive)
|
|
168
|
+
sources.archivedFiles += 1;
|
|
169
|
+
else
|
|
170
|
+
sources.activeFiles += 1;
|
|
171
|
+
const lines = fs_1.default.readFileSync(target, 'utf-8').split('\n');
|
|
172
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
173
|
+
const raw = lines[index];
|
|
174
|
+
if (!raw.trim())
|
|
175
|
+
continue;
|
|
176
|
+
if (linesSeen >= limits.maxEntries) {
|
|
177
|
+
skip('entry-limit');
|
|
178
|
+
entryLimitReached = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
linesSeen += 1;
|
|
182
|
+
if (Buffer.byteLength(raw, 'utf-8') > limits.maxLineBytes) {
|
|
183
|
+
skip('line-too-large');
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!isJsonDepthWithinLimit(raw, limits.maxJsonDepth)) {
|
|
187
|
+
skip('json-too-deep');
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let value;
|
|
191
|
+
try {
|
|
192
|
+
value = JSON.parse(raw);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
skip('invalid-json');
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const source = relativeSource(root, target, index + 1);
|
|
199
|
+
const parsed = (0, types_1.parseLedgerEntry)(value, source);
|
|
200
|
+
if (!parsed.ok) {
|
|
201
|
+
skip(parsed.reason);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
sources.validEntries += 1;
|
|
205
|
+
if (parsed.entry.polarity === 'finding') {
|
|
206
|
+
sources.validFindings += 1;
|
|
207
|
+
const defectClass = parsed.entry.defectClass ?? 'unclassified';
|
|
208
|
+
const count = evidenceRefsByClass.get(defectClass) ?? 0;
|
|
209
|
+
const evidenceRef = count < limits.maxRefsPerClass ? source : null;
|
|
210
|
+
if (evidenceRef === null) {
|
|
211
|
+
omittedEvidenceRefs += 1;
|
|
212
|
+
// The finding remains available for recurrence analysis, but
|
|
213
|
+
// its public evidence is deliberately omitted. Record the
|
|
214
|
+
// truncation through the same typed source contract as every
|
|
215
|
+
// other scan bound so downstream renderers cannot call it a
|
|
216
|
+
// complete report.
|
|
217
|
+
skip('evidence-ref-limit');
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
evidenceRefsByClass.set(defectClass, count + 1);
|
|
221
|
+
entries.push({ entry: parsed.entry, source, evidenceRef });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!entryLimitReached && collected.truncated)
|
|
226
|
+
skip('file-limit');
|
|
227
|
+
return { entries, sources, omittedEvidenceRefs };
|
|
228
|
+
}
|
|
@@ -12,8 +12,75 @@ exports.archiveLedger = archiveLedger;
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const child_process_1 = require("child_process");
|
|
15
|
+
const types_1 = require("./types");
|
|
15
16
|
const cluster_1 = require("./cluster");
|
|
16
17
|
const LEDGER_DIR = path_1.default.join('.awm', 'ledger');
|
|
18
|
+
function isWithin(root, candidate) {
|
|
19
|
+
const relative = path_1.default.relative(root, candidate);
|
|
20
|
+
return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
|
|
21
|
+
}
|
|
22
|
+
function assertSafeDirectory(directory, root, name) {
|
|
23
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
24
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
25
|
+
throw new Error(`unsafe ledger ${name} directory`);
|
|
26
|
+
const real = fs_1.default.realpathSync(directory);
|
|
27
|
+
if (!isWithin(root, real))
|
|
28
|
+
throw new Error(`ledger ${name} directory escapes its root`);
|
|
29
|
+
return real;
|
|
30
|
+
}
|
|
31
|
+
function sourceExists(source) {
|
|
32
|
+
try {
|
|
33
|
+
fs_1.default.lstatSync(source);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return false;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function assertSafeLedgerSource(source, ledgerRoot) {
|
|
43
|
+
const stat = fs_1.default.lstatSync(source);
|
|
44
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
45
|
+
throw new Error('unsafe ledger source');
|
|
46
|
+
const real = fs_1.default.realpathSync(source);
|
|
47
|
+
if (!isWithin(ledgerRoot, real))
|
|
48
|
+
throw new Error('ledger source escapes ledger directory');
|
|
49
|
+
}
|
|
50
|
+
function ledgerFilename(branch) {
|
|
51
|
+
if (typeof branch !== 'string'
|
|
52
|
+
|| branch.length === 0
|
|
53
|
+
|| branch.includes('\\')
|
|
54
|
+
|| path_1.default.posix.isAbsolute(branch)
|
|
55
|
+
|| path_1.default.win32.isAbsolute(branch)
|
|
56
|
+
|| branch.split('/').some(segment => segment === '.' || segment === '..')) {
|
|
57
|
+
throw new Error('invalid ledger branch');
|
|
58
|
+
}
|
|
59
|
+
return branch.replace(/\//g, '__');
|
|
60
|
+
}
|
|
61
|
+
function archiveLabel(label) {
|
|
62
|
+
if (typeof label !== 'string'
|
|
63
|
+
|| label.length === 0
|
|
64
|
+
|| label === '.'
|
|
65
|
+
|| label.includes('..')
|
|
66
|
+
|| /[/\\]/.test(label)
|
|
67
|
+
|| path_1.default.posix.isAbsolute(label)
|
|
68
|
+
|| path_1.default.win32.isAbsolute(label)) {
|
|
69
|
+
throw new Error('invalid ledger label');
|
|
70
|
+
}
|
|
71
|
+
return label;
|
|
72
|
+
}
|
|
73
|
+
function destinationExists(destination) {
|
|
74
|
+
try {
|
|
75
|
+
fs_1.default.lstatSync(destination);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === 'ENOENT')
|
|
80
|
+
return false;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
17
84
|
function detectBranch(cwd) {
|
|
18
85
|
try {
|
|
19
86
|
const b = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
@@ -26,28 +93,15 @@ function detectBranch(cwd) {
|
|
|
26
93
|
}
|
|
27
94
|
}
|
|
28
95
|
function ledgerPath(cwd, branch) {
|
|
29
|
-
|
|
30
|
-
return path_1.default.join(cwd, LEDGER_DIR, `${safe}.jsonl`);
|
|
96
|
+
return path_1.default.join(cwd, LEDGER_DIR, `${ledgerFilename(branch)}.jsonl`);
|
|
31
97
|
}
|
|
32
98
|
function addEntry(cwd, entry) {
|
|
99
|
+
const parsed = (0, types_1.parseLedgerEntry)(entry, 'ledger entry');
|
|
100
|
+
if (!parsed.ok)
|
|
101
|
+
throw new Error(`invalid ledger entry: ${parsed.reason}`);
|
|
33
102
|
const p = ledgerPath(cwd, entry.branch);
|
|
34
103
|
fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
|
|
35
|
-
fs_1.default.appendFileSync(p, JSON.stringify(entry) + '\n', 'utf-8');
|
|
36
|
-
}
|
|
37
|
-
/** Required LedgerEntry fields that `cluster.ts` reads unconditionally
|
|
38
|
-
* (`signature`, `desc`, `ref` when present). A JSONL line can be syntactically
|
|
39
|
-
* valid JSON while still being shape-invalid (e.g. missing `desc`) — that's
|
|
40
|
-
* not a parse error, so it needs its own check, extending the same "skip
|
|
41
|
-
* malformed line" policy this function already applies to JSON syntax errors. */
|
|
42
|
-
function isWellFormedEntry(x) {
|
|
43
|
-
if (!x || typeof x !== 'object')
|
|
44
|
-
return false;
|
|
45
|
-
const e = x;
|
|
46
|
-
return typeof e.signature === 'string'
|
|
47
|
-
&& typeof e.desc === 'string'
|
|
48
|
-
&& typeof e.branch === 'string'
|
|
49
|
-
&& typeof e.polarity === 'string'
|
|
50
|
-
&& (e.ref === undefined || typeof e.ref === 'string');
|
|
104
|
+
fs_1.default.appendFileSync(p, JSON.stringify(parsed.entry) + '\n', 'utf-8');
|
|
51
105
|
}
|
|
52
106
|
function listEntries(cwd, branch) {
|
|
53
107
|
const p = ledgerPath(cwd, branch);
|
|
@@ -59,9 +113,9 @@ function listEntries(cwd, branch) {
|
|
|
59
113
|
if (!trimmed)
|
|
60
114
|
continue;
|
|
61
115
|
try {
|
|
62
|
-
const parsed = JSON.parse(trimmed);
|
|
63
|
-
if (
|
|
64
|
-
out.push(parsed);
|
|
116
|
+
const parsed = (0, types_1.parseLedgerEntry)(JSON.parse(trimmed), p);
|
|
117
|
+
if (parsed.ok)
|
|
118
|
+
out.push(parsed.entry);
|
|
65
119
|
}
|
|
66
120
|
catch { /* skip malformed line */ }
|
|
67
121
|
}
|
|
@@ -71,12 +125,22 @@ function recurring(cwd, branch, min) {
|
|
|
71
125
|
return (0, cluster_1.clusterEntries)(listEntries(cwd, branch), min);
|
|
72
126
|
}
|
|
73
127
|
function archiveLedger(cwd, branch, label) {
|
|
128
|
+
const safeLabel = archiveLabel(label);
|
|
74
129
|
const src = ledgerPath(cwd, branch);
|
|
75
|
-
if (!
|
|
130
|
+
if (!sourceExists(src))
|
|
76
131
|
return false;
|
|
77
|
-
const safe = branch
|
|
78
|
-
const
|
|
79
|
-
|
|
132
|
+
const safe = ledgerFilename(branch);
|
|
133
|
+
const projectRoot = fs_1.default.realpathSync(cwd);
|
|
134
|
+
const ledgerRoot = path_1.default.join(projectRoot, LEDGER_DIR);
|
|
135
|
+
const safeLedgerRoot = assertSafeDirectory(ledgerRoot, projectRoot, 'root');
|
|
136
|
+
assertSafeLedgerSource(src, safeLedgerRoot);
|
|
137
|
+
const archiveRoot = path_1.default.join(ledgerRoot, 'archive');
|
|
138
|
+
if (!sourceExists(archiveRoot))
|
|
139
|
+
fs_1.default.mkdirSync(archiveRoot);
|
|
140
|
+
const safeArchiveRoot = assertSafeDirectory(archiveRoot, safeLedgerRoot, 'archive');
|
|
141
|
+
const dst = path_1.default.join(safeArchiveRoot, `${safe}-${safeLabel}.jsonl`);
|
|
142
|
+
if (destinationExists(dst))
|
|
143
|
+
throw new Error(`ledger archive already exists: ${dst}`);
|
|
80
144
|
fs_1.default.renameSync(src, dst);
|
|
81
145
|
return true;
|
|
82
146
|
}
|
|
@@ -1,2 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFECT_CLASS = void 0;
|
|
4
|
+
exports.parseLedgerEntry = parseLedgerEntry;
|
|
5
|
+
exports.DEFECT_CLASS = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function isPolarity(value) {
|
|
10
|
+
return value === 'win' || value === 'finding';
|
|
11
|
+
}
|
|
12
|
+
function isLedgerClass(value) {
|
|
13
|
+
return value === 'structural' || value === 'logica' || value === 'proceso' || value === 'seguridad';
|
|
14
|
+
}
|
|
15
|
+
function isSeverity(value) {
|
|
16
|
+
return value === 'blocker' || value === 'important' || value === 'minor' || value === 'info';
|
|
17
|
+
}
|
|
18
|
+
/** Validates the durable JSONL boundary. Missing defectClass is deliberately legacy-valid. */
|
|
19
|
+
function parseLedgerEntry(input, source) {
|
|
20
|
+
if (typeof source !== 'string')
|
|
21
|
+
throw new Error('ledger parse source must be a string');
|
|
22
|
+
if (!isRecord(input))
|
|
23
|
+
return { ok: false, source, reason: 'not-object' };
|
|
24
|
+
if (!isPolarity(input.polarity) || !isLedgerClass(input.class) || !isSeverity(input.severity)) {
|
|
25
|
+
return { ok: false, source, reason: 'invalid-enum' };
|
|
26
|
+
}
|
|
27
|
+
for (const field of ['ts', 'branch', 'phase', 'source_skill', 'signature', 'desc']) {
|
|
28
|
+
if (typeof input[field] !== 'string')
|
|
29
|
+
return { ok: false, source, reason: 'invalid-fields' };
|
|
30
|
+
}
|
|
31
|
+
if ('ref' in input && input.ref !== undefined && typeof input.ref !== 'string') {
|
|
32
|
+
return { ok: false, source, reason: 'invalid-ref' };
|
|
33
|
+
}
|
|
34
|
+
if ('defectClass' in input && input.defectClass !== undefined
|
|
35
|
+
&& (typeof input.defectClass !== 'string' || !exports.DEFECT_CLASS.test(input.defectClass))) {
|
|
36
|
+
return { ok: false, source, reason: 'invalid-defect-class' };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
entry: {
|
|
41
|
+
ts: input.ts,
|
|
42
|
+
branch: input.branch,
|
|
43
|
+
phase: input.phase,
|
|
44
|
+
source_skill: input.source_skill,
|
|
45
|
+
polarity: input.polarity,
|
|
46
|
+
class: input.class,
|
|
47
|
+
signature: input.signature,
|
|
48
|
+
severity: input.severity,
|
|
49
|
+
desc: input.desc,
|
|
50
|
+
...(typeof input.ref === 'string' ? { ref: input.ref } : {}),
|
|
51
|
+
// Keep the durable legacy distinction visible to consumers: an older
|
|
52
|
+
// entry is valid, but has no reusable classification yet.
|
|
53
|
+
defectClass: typeof input.defectClass === 'string' ? input.defectClass : undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/src/core/paths.js
CHANGED
|
@@ -174,10 +174,11 @@ function resolveOnPath(bin) {
|
|
|
174
174
|
* cohorte no llegaba nunca ni a `SERIAL` ni a `ACTIVE`. Un solo bug de comparación, quince
|
|
175
175
|
* tests en cascada.
|
|
176
176
|
*
|
|
177
|
-
* La identidad se toma
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
177
|
+
* La identidad se toma primero de `realpathSync.native`, que resuelve la ruta con la API
|
|
178
|
+
* del SO (incluyendo nombres cortos 8.3) antes de normalizar separadores y capitalización.
|
|
179
|
+
* En POSIX, dos hard links de archivo conservan el mismo `(dev, ino)` aunque el realpath
|
|
180
|
+
* mantenga sus nombres distintos; ese fallback preserva su identidad. En Windows no se usa:
|
|
181
|
+
* dos directorios temporales distintos pueden reportar el mismo par de metadatos.
|
|
181
182
|
*
|
|
182
183
|
* Cualquier fallo de `stat` significa que al menos una no existe: `false`. La identidad
|
|
183
184
|
* nunca se afirma sin prueba.
|
|
@@ -192,10 +193,6 @@ function sameExistingPath(a, b) {
|
|
|
192
193
|
catch {
|
|
193
194
|
return false;
|
|
194
195
|
}
|
|
195
|
-
// `ino === 0` en AMBOS lados no prueba identidad: probaría que el filesystem no la
|
|
196
|
-
// reporta. Tratarlo como match haría coincidir dos rutas cualesquiera del mismo volumen.
|
|
197
|
-
if (sa.ino !== 0 && sb.ino !== 0)
|
|
198
|
-
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
199
196
|
const canonical = (p) => {
|
|
200
197
|
try {
|
|
201
198
|
const real = fs_1.default.realpathSync.native(p).replaceAll('\\', '/');
|
|
@@ -207,5 +204,9 @@ function sameExistingPath(a, b) {
|
|
|
207
204
|
};
|
|
208
205
|
const ca = canonical(a);
|
|
209
206
|
const cb = canonical(b);
|
|
210
|
-
|
|
207
|
+
if (ca === null || cb === null)
|
|
208
|
+
return false;
|
|
209
|
+
if (ca === cb)
|
|
210
|
+
return true;
|
|
211
|
+
return !isWindowsNative() && sa.ino !== 0 && sb.ino !== 0 && sa.dev === sb.dev && sa.ino === sb.ino;
|
|
211
212
|
}
|