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
|
@@ -14,6 +14,7 @@ const resolve_1 = require("./compatibility/resolve");
|
|
|
14
14
|
const project_1 = require("./project");
|
|
15
15
|
const source_1 = require("./compatibility/source");
|
|
16
16
|
const registries_1 = require("../../core/registries");
|
|
17
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
17
18
|
/** First non-flag token after `npx` — the tool the command actually runs. */
|
|
18
19
|
function npxTool(parts) {
|
|
19
20
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -197,6 +198,90 @@ function invalidStatus(project, pack, reason = 'manifest-malformed', remedy = 'r
|
|
|
197
198
|
...projectMetadata(project, 'invalid', reason, { remedy }),
|
|
198
199
|
};
|
|
199
200
|
}
|
|
201
|
+
function hasSelectedPack(manifest) {
|
|
202
|
+
return typeof manifest.pack === 'string' && manifest.pack.length > 0;
|
|
203
|
+
}
|
|
204
|
+
function projectSensorCheck(sensor, cwd) {
|
|
205
|
+
if (sensor.enabled === false) {
|
|
206
|
+
return { ok: true, detail: 'disabled (project-declared)', certification: 'project-declared' };
|
|
207
|
+
}
|
|
208
|
+
const check = checkStructuredCommand(sensor.command, cwd, sensor.assets);
|
|
209
|
+
return {
|
|
210
|
+
ok: check.ok,
|
|
211
|
+
detail: check.ok ? `project-declared: ${check.detail}` : `${check.detail} (project-declared)`,
|
|
212
|
+
certification: 'project-declared',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function packBoundOnly(manifest) {
|
|
216
|
+
const sensors = Object.fromEntries(Object.entries(manifest.sensors).filter(([, sensor]) => !(0, manifest_1.isProjectDeclaredSensor)(sensor)));
|
|
217
|
+
const pack = 'pack' in manifest && typeof manifest.pack === 'string' ? manifest.pack : '';
|
|
218
|
+
return { schemaVersion: 2, pack, sensors };
|
|
219
|
+
}
|
|
220
|
+
/** Live pack sensor ids, or null when the pack cannot be resolved (fail closed). */
|
|
221
|
+
function livePackSensorIds(source) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
224
|
+
if (parsed.kind !== 'v2')
|
|
225
|
+
return null;
|
|
226
|
+
return new Set(Object.keys(parsed.pack.sensors));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* RF-1.7 status: never label a colliding project id as project-declared READY.
|
|
234
|
+
* When pack is selected and live is unavailable, fail closed rather than claiming
|
|
235
|
+
* the project override is safe.
|
|
236
|
+
*/
|
|
237
|
+
function projectDeclaredOrCollisionCheck(name, sensor, packageRoot, packSelected, liveIds) {
|
|
238
|
+
if (packSelected && (liveIds === null || liveIds.has(name))) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
detail: liveIds === null
|
|
242
|
+
? `project-sensor-name-collision: ${name} (live pack unavailable)`
|
|
243
|
+
: `project-sensor-name-collision: ${name}; pack sensor kept`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return projectSensorCheck(sensor, packageRoot);
|
|
247
|
+
}
|
|
248
|
+
/** Soft-isolate valid siblings into checks while authority remains invalid (RF soft-parse). */
|
|
249
|
+
function softIsolatedChecks(project) {
|
|
250
|
+
const recovered = (0, project_1.softRecoverInvalidProject)(project);
|
|
251
|
+
if (!recovered)
|
|
252
|
+
return {};
|
|
253
|
+
const checks = {};
|
|
254
|
+
const v3Project = recovered.manifest.kind === 'v3' && recovered.manifest.pack.mode === 'project-sensors'
|
|
255
|
+
? recovered.manifest.pack
|
|
256
|
+
: null;
|
|
257
|
+
const sensors = v3Project?.sensors ?? {};
|
|
258
|
+
// RF-1.7: same collision path as configured status — live pack probe when pack is selected.
|
|
259
|
+
const packSelected = v3Project !== null && hasSelectedPack(v3Project);
|
|
260
|
+
let liveIds = null;
|
|
261
|
+
if (packSelected) {
|
|
262
|
+
try {
|
|
263
|
+
const resolution = (0, source_1.resolveSensorSource)(recovered.manifest, { registries: (0, registries_1.listRegistries)() });
|
|
264
|
+
liveIds = 'source' in resolution ? livePackSensorIds(resolution.source) : null;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
liveIds = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const [name, sensor] of Object.entries(sensors)) {
|
|
271
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
272
|
+
checks[name] = projectDeclaredOrCollisionCheck(name, sensor, recovered.packageRoot, packSelected, liveIds);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const packSensor = sensor;
|
|
276
|
+
checks[name] = packSensor.enabled === false
|
|
277
|
+
? { ok: true, detail: 'disabled' }
|
|
278
|
+
: checkStructuredCommand(packSensor.command, recovered.packageRoot, packSensor.assets);
|
|
279
|
+
}
|
|
280
|
+
for (const entry of recovered.invalidEntries) {
|
|
281
|
+
checks[entry.name] = { ok: false, detail: entry.reason };
|
|
282
|
+
}
|
|
283
|
+
return checks;
|
|
284
|
+
}
|
|
200
285
|
async function computeSensorStatus(cwd = process.cwd()) {
|
|
201
286
|
let project;
|
|
202
287
|
try {
|
|
@@ -211,15 +296,40 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
211
296
|
}
|
|
212
297
|
if (project.state === 'invalid') {
|
|
213
298
|
let pack = null;
|
|
299
|
+
let reason = 'manifest-malformed';
|
|
300
|
+
let remedy = 'repair-sensor-manifest';
|
|
214
301
|
try {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
302
|
+
const text = fs_1.default.readFileSync(project.manifestPath, 'utf-8');
|
|
303
|
+
try {
|
|
304
|
+
const raw = JSON.parse(text);
|
|
305
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
306
|
+
const record = raw;
|
|
307
|
+
if (!('schemaVersion' in record) && typeof record.pack === 'string')
|
|
308
|
+
pack = record.pack;
|
|
309
|
+
else if (typeof record.pack === 'string')
|
|
310
|
+
pack = record.pack;
|
|
311
|
+
}
|
|
312
|
+
// JSON.parse succeeded — this is schema invalidity, not "not valid JSON" (RF-4.1).
|
|
313
|
+
if (project.reason.includes('unsupported manifest schemaVersion')) {
|
|
314
|
+
reason = 'schema-unsupported';
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
reason = `schema-invalid: ${project.reason}`;
|
|
318
|
+
// Project-entry schema failures must not lead with `awm sensors init` (RF-3.3).
|
|
319
|
+
remedy = 'repair-the-invalid-sensor-entry-in-sensors-json';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
reason = 'manifest-malformed';
|
|
218
324
|
}
|
|
219
325
|
}
|
|
220
|
-
catch { /*
|
|
221
|
-
|
|
222
|
-
|
|
326
|
+
catch { /* unreadable manifest stays malformed */ }
|
|
327
|
+
// Soft-isolate valid siblings into checks (same WithIssues path as run) while
|
|
328
|
+
// keeping mode=invalid so preflight RF-4.1 still sees schema-invalid authority.
|
|
329
|
+
return {
|
|
330
|
+
...invalidStatus(project, pack, reason, remedy),
|
|
331
|
+
checks: softIsolatedChecks(project),
|
|
332
|
+
};
|
|
223
333
|
}
|
|
224
334
|
const packageRoot = project.packageRoot;
|
|
225
335
|
const parsed = project.manifest;
|
|
@@ -229,38 +339,73 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
229
339
|
...projectMetadata(project, parsed.pack.mode, `${parsed.pack.mode}-declared`, { declarationReason: parsed.pack.reason }),
|
|
230
340
|
};
|
|
231
341
|
}
|
|
342
|
+
if (parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors' && !hasSelectedPack(parsed.pack)) {
|
|
343
|
+
// Only-project: no pack source to resolve. Report each project sensor with
|
|
344
|
+
// project-declared provenance rather than an empty Pack:none DEGRADED (RF-2.8).
|
|
345
|
+
const checks = {};
|
|
346
|
+
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
347
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
348
|
+
checks[name] = projectSensorCheck(sensor, packageRoot);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
checks[name] = { ok: false, detail: 'pack-bound sensor requires a nonempty pack' };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
356
|
+
pack: null,
|
|
357
|
+
checks,
|
|
358
|
+
...projectMetadata(project, 'project-sensors', 'project-declared-only'),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
232
361
|
if (parsed.kind === 'v2' || parsed.kind === 'v3') {
|
|
233
362
|
const parsedPack = parsed.pack;
|
|
363
|
+
const packId = typeof parsedPack.pack === 'string' ? parsedPack.pack : null;
|
|
364
|
+
const packSelected = typeof parsedPack.pack === 'string' && parsedPack.pack.length > 0;
|
|
234
365
|
let resolution;
|
|
235
366
|
try {
|
|
236
367
|
resolution = (0, source_1.resolveSensorSource)(parsed, { registries: (0, registries_1.listRegistries)() });
|
|
237
368
|
}
|
|
238
369
|
catch {
|
|
239
|
-
return invalidStatus(project,
|
|
370
|
+
return invalidStatus(project, packId, 'sensor-source-invalid', 'repair-or-run-awm-update');
|
|
240
371
|
}
|
|
241
372
|
if (!('source' in resolution))
|
|
242
|
-
return sourceFailure(project,
|
|
373
|
+
return sourceFailure(project, packId ?? 'unknown', resolution);
|
|
243
374
|
const sourceMeta = resolvedSourceMetadata(resolution);
|
|
244
|
-
const
|
|
375
|
+
const liveIds = livePackSensorIds(resolution.source);
|
|
376
|
+
const packManifest = packBoundOnly(parsedPack);
|
|
245
377
|
const checks = {};
|
|
246
378
|
let compatibility;
|
|
247
379
|
try {
|
|
248
|
-
compatibility = resolveStaticV2Compatibility(packageRoot,
|
|
380
|
+
compatibility = resolveStaticV2Compatibility(packageRoot, packManifest, resolution.source);
|
|
249
381
|
}
|
|
250
382
|
catch (error) {
|
|
251
383
|
const detail = error instanceof Error ? error.message : 'live compatibility unavailable';
|
|
252
|
-
for (const [name, sensor] of Object.entries(
|
|
253
|
-
|
|
254
|
-
|
|
384
|
+
for (const [name, sensor] of Object.entries(parsedPack.sensors)) {
|
|
385
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
386
|
+
checks[name] = projectDeclaredOrCollisionCheck(name, sensor, packageRoot, packSelected, liveIds);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
checks[name] = sensor.enabled === false
|
|
390
|
+
? { ok: true, detail: 'disabled' }
|
|
391
|
+
: { ok: false, detail };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { overall: 'DEGRADED', pack: packId, checks, ...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }) };
|
|
255
395
|
}
|
|
256
|
-
for (const [name, sensor] of Object.entries(
|
|
257
|
-
|
|
258
|
-
|
|
396
|
+
for (const [name, sensor] of Object.entries(parsedPack.sensors)) {
|
|
397
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
398
|
+
checks[name] = projectDeclaredOrCollisionCheck(name, sensor, packageRoot, packSelected, liveIds);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const packSensor = sensor;
|
|
402
|
+
checks[name] = packSensor.enabled === false ? { ok: true, detail: 'disabled' }
|
|
403
|
+
: staticCompatibilityCheck(packSensor, compatibility[name]) ?? checkStructuredCommand(packSensor.command, packageRoot, packSensor.assets);
|
|
259
404
|
}
|
|
260
|
-
const uninitialized = uninitializedSensors(
|
|
405
|
+
const uninitialized = uninitializedSensors(packManifest, compatibility);
|
|
261
406
|
return {
|
|
262
407
|
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
263
|
-
pack:
|
|
408
|
+
pack: packId, checks,
|
|
264
409
|
...(uninitialized ? { uninitialized } : {}),
|
|
265
410
|
...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }),
|
|
266
411
|
};
|
|
@@ -160,6 +160,9 @@ function registerWatchCommand(program) {
|
|
|
160
160
|
}
|
|
161
161
|
else {
|
|
162
162
|
process.stdout.write(`awm watch: ${outcome === 'stopped' ? 'detenido' : 'track congelado'}; ciclo ${final.cycleState ?? final.state} — sin certificacion de cierre\n`);
|
|
163
|
+
// exitCode only — never process.exit here: in-process tests call parseAsync
|
|
164
|
+
// inside Jest; a hard exit kills the worker (CI exit 130 on every OS).
|
|
165
|
+
// exitCode=130 is intentional Darwin CI flake harden (reopened-196), not operator-environments scope.
|
|
163
166
|
if (outcome === 'stopped')
|
|
164
167
|
process.exitCode = 130;
|
|
165
168
|
else if (outcome === 'complete' || final.state !== 'present')
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONTEXT_INJECT_MAX_CHARS = void 0;
|
|
4
|
+
exports.statusFor = statusFor;
|
|
5
|
+
exports.buildInjection = buildInjection;
|
|
6
|
+
/** Fixed budget for `inject: context` fresh values (RF-3.2). */
|
|
7
|
+
exports.CONTEXT_INJECT_MAX_CHARS = 2048;
|
|
8
|
+
function assertValidNow(now) {
|
|
9
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
|
|
10
|
+
throw new Error('now must be a Date with a finite getTime()');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function effectiveStatus(definitionDurability, stored, now) {
|
|
14
|
+
if (!stored)
|
|
15
|
+
return 'missing';
|
|
16
|
+
if (stored.status === 'missing')
|
|
17
|
+
return 'missing';
|
|
18
|
+
if (stored.status === 'stale')
|
|
19
|
+
return 'stale';
|
|
20
|
+
// Durable never time-stales (RF-2.3).
|
|
21
|
+
if (definitionDurability === 'durable') {
|
|
22
|
+
return stored.status === 'fresh' ? 'fresh' : stored.status;
|
|
23
|
+
}
|
|
24
|
+
// Volatile: fail closed — missing/invalid expiresAt or now >= expiresAt → stale (RF-2.4).
|
|
25
|
+
if (typeof stored.expiresAt !== 'string' || stored.expiresAt.trim() === '') {
|
|
26
|
+
return 'stale';
|
|
27
|
+
}
|
|
28
|
+
const expiresMs = Date.parse(stored.expiresAt);
|
|
29
|
+
if (!Number.isFinite(expiresMs) || now.getTime() >= expiresMs) {
|
|
30
|
+
return 'stale';
|
|
31
|
+
}
|
|
32
|
+
return stored.status === 'fresh' ? 'fresh' : stored.status;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Per-catalog-id status against the store. Missing → `missing` with no default (RF-2.5).
|
|
36
|
+
*/
|
|
37
|
+
function statusFor(catalog, store, now = new Date()) {
|
|
38
|
+
if (!catalog || typeof catalog !== 'object' || !catalog.facts) {
|
|
39
|
+
throw new Error('statusFor: catalog with facts is required');
|
|
40
|
+
}
|
|
41
|
+
if (!store || typeof store !== 'object') {
|
|
42
|
+
throw new Error('statusFor: store must be an object');
|
|
43
|
+
}
|
|
44
|
+
assertValidNow(now);
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [id, def] of Object.entries(catalog.facts)) {
|
|
47
|
+
out[id] = effectiveStatus(def.durability, store[id], now);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build injection payload. Default / explicit `reference` exposes path + ids only (RF-3.1).
|
|
53
|
+
* `context` includes fresh `inject: context` values under budget; overflow degrades (RF-3.2).
|
|
54
|
+
*/
|
|
55
|
+
function buildInjection(opts) {
|
|
56
|
+
if (!opts || typeof opts !== 'object') {
|
|
57
|
+
throw new Error('buildInjection: options object is required');
|
|
58
|
+
}
|
|
59
|
+
if (!opts.catalog?.facts) {
|
|
60
|
+
throw new Error('buildInjection: catalog with facts is required');
|
|
61
|
+
}
|
|
62
|
+
if (typeof opts.storePath !== 'string' || opts.storePath.trim() === '') {
|
|
63
|
+
throw new Error('buildInjection: storePath must be a non-empty string');
|
|
64
|
+
}
|
|
65
|
+
if (!opts.store || typeof opts.store !== 'object') {
|
|
66
|
+
throw new Error('buildInjection: store must be an object');
|
|
67
|
+
}
|
|
68
|
+
const rawInject = opts.inject ?? 'reference';
|
|
69
|
+
// Unknown/typo must not fall through into context (fail-open). Prefer reference (RNF-2).
|
|
70
|
+
const inject = rawInject === 'reference' || rawInject === 'context' ? rawInject : 'reference';
|
|
71
|
+
const maxChars = opts.maxChars ?? exports.CONTEXT_INJECT_MAX_CHARS;
|
|
72
|
+
if (typeof maxChars !== 'number' || !Number.isFinite(maxChars) || maxChars < 0) {
|
|
73
|
+
throw new Error('buildInjection: maxChars must be a non-negative finite number');
|
|
74
|
+
}
|
|
75
|
+
const now = opts.now ?? new Date();
|
|
76
|
+
assertValidNow(now);
|
|
77
|
+
if (opts.factIds !== undefined) {
|
|
78
|
+
if (!Array.isArray(opts.factIds) || opts.factIds.some((id) => typeof id !== 'string' || id === '')) {
|
|
79
|
+
throw new Error('buildInjection: factIds must be an array of non-empty strings');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const factIds = opts.factIds ?? Object.keys(opts.catalog.facts);
|
|
83
|
+
const statuses = statusFor(opts.catalog, opts.store, now);
|
|
84
|
+
const referenceResult = () => ({
|
|
85
|
+
mode: 'reference',
|
|
86
|
+
storePath: opts.storePath,
|
|
87
|
+
factIds: [...factIds],
|
|
88
|
+
});
|
|
89
|
+
if (inject !== 'context') {
|
|
90
|
+
return referenceResult();
|
|
91
|
+
}
|
|
92
|
+
// Context mode: only fresh values whose catalog inject is `context` (RF-3.2).
|
|
93
|
+
const contextValues = {};
|
|
94
|
+
let totalChars = 0;
|
|
95
|
+
for (const id of factIds) {
|
|
96
|
+
const def = opts.catalog.facts[id];
|
|
97
|
+
if (!def || def.inject !== 'context')
|
|
98
|
+
continue;
|
|
99
|
+
if (statuses[id] !== 'fresh')
|
|
100
|
+
continue;
|
|
101
|
+
const stored = opts.store[id];
|
|
102
|
+
if (!stored || typeof stored.value !== 'string')
|
|
103
|
+
continue;
|
|
104
|
+
totalChars += stored.value.length;
|
|
105
|
+
contextValues[id] = stored.value;
|
|
106
|
+
}
|
|
107
|
+
if (Object.keys(contextValues).length === 0) {
|
|
108
|
+
return referenceResult();
|
|
109
|
+
}
|
|
110
|
+
if (totalChars > maxChars) {
|
|
111
|
+
return {
|
|
112
|
+
...referenceResult(),
|
|
113
|
+
warning: `Context inject budget exceeded (${totalChars} > ${maxChars} chars); degraded to reference`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
mode: 'context',
|
|
118
|
+
storePath: opts.storePath,
|
|
119
|
+
factIds: [...factIds],
|
|
120
|
+
values: contextValues,
|
|
121
|
+
};
|
|
122
|
+
}
|