@aiwg/cli 2026.10.6 → 2026.10.8
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/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +36 -10
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +122 -109
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +4 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
package/dist/src/decision/ask.js
CHANGED
|
@@ -495,6 +495,11 @@ export async function setupJev(input = {}, options = {}) {
|
|
|
495
495
|
next: ['Run aiwg decision ask --question "<q>" --yes-no --json.'],
|
|
496
496
|
};
|
|
497
497
|
if (input.verify) {
|
|
498
|
+
// Verify the credential just saved, even when the shell still names an
|
|
499
|
+
// older token. Ordinary ask calls retain their environment precedence.
|
|
500
|
+
const verificationEnv = { ...environment };
|
|
501
|
+
delete verificationEnv.JEV_API_KEY;
|
|
502
|
+
delete verificationEnv.AIWG_DECISION_JEV_API_KEY;
|
|
498
503
|
const verified = await askDecision({
|
|
499
504
|
question: 'Is this Jev decision setup verification request well formed?',
|
|
500
505
|
mode: { kind: 'yes-no' },
|
|
@@ -502,7 +507,7 @@ export async function setupJev(input = {}, options = {}) {
|
|
|
502
507
|
timeoutMs: 15_000,
|
|
503
508
|
passes: 1,
|
|
504
509
|
escalationPasses: 1,
|
|
505
|
-
}, options);
|
|
510
|
+
}, { ...options, env: verificationEnv });
|
|
506
511
|
result.verified = verified.status === 'answered';
|
|
507
512
|
result.model = verified.model;
|
|
508
513
|
result.usage = verified.usage;
|
|
@@ -57,7 +57,7 @@ export function benchmarkComparison(capture, pricing) {
|
|
|
57
57
|
return `| Jev, ${passes} pass${passes === 1 ? '' : 'es'} | ${display(t.input)} | n/a | ${display(t.output)} | ${display(t.input === null || t.output === null ? null : t.input + t.output)} | ${t.latency.toFixed(1)} | ${money(t.cost)} |`;
|
|
58
58
|
}),
|
|
59
59
|
`| ${capture.baselineLabel.replace(/[|\r\n]/g, ' ')} | ${display(baseline.input)} | ${display(baseline.cached)} | ${display(baseline.output)} | ${display(baseline.input === null || baseline.output === null ? null : baseline.input + baseline.output)} | ${baseline.latency.toFixed(1)} | ${money(baseline.cost)} |`, '',
|
|
60
|
-
capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
|
|
60
|
+
capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use provider-reported USD or sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
|
|
61
61
|
'Costs use reported usage and explicit pricing assumptions, not subscription invoices.',
|
|
62
62
|
'Standalone baseline requests do not measure the marginal cost of an inline LLM decision.',
|
|
63
63
|
];
|
|
@@ -116,6 +116,9 @@ function validCost(sample) {
|
|
|
116
116
|
if (sample.cost === undefined)
|
|
117
117
|
return true;
|
|
118
118
|
const cost = sample.cost;
|
|
119
|
+
if (cost?.source === 'provider-reported')
|
|
120
|
+
return fields(cost, ['usd', 'price', 'source'])
|
|
121
|
+
&& cost.price === null && typeof cost.usd === 'number' && Number.isFinite(cost.usd) && cost.usd >= 0;
|
|
119
122
|
return fields(cost, ['usd', 'price']) && (cost.price === null ? cost.usd === null
|
|
120
123
|
: validPrice(cost.price) && cost.price.model === sample.model && cost.usd === tokenCost(sample.usage, cost.price));
|
|
121
124
|
}
|
|
@@ -128,10 +131,18 @@ export async function collectBenchmark(dataset, options) {
|
|
|
128
131
|
throw new Error('Positive maxUsd required for live collection.');
|
|
129
132
|
if (!Number.isInteger(options.passes) || options.passes < 1 || options.passes > MAX_BENCHMARK_PASSES)
|
|
130
133
|
throw new Error('passes must be 1..20.');
|
|
134
|
+
if (options.jevCapture && options.extendCapture)
|
|
135
|
+
throw new Error('Cannot combine Jev reuse and capture extension.');
|
|
131
136
|
const cases = splitBenchmarkHoldout(dataset, options.holdout ?? 0.3, options.seed ?? '3291');
|
|
132
137
|
const prior = options.extendCapture;
|
|
133
138
|
if (prior)
|
|
134
139
|
validateBenchmarkCapture(cases, prior);
|
|
140
|
+
const reused = options.jevCapture;
|
|
141
|
+
if (reused) {
|
|
142
|
+
validateBenchmarkCapture(cases, reused);
|
|
143
|
+
if (reused.rows.length !== cases.length || reused.rows.some(row => row.jev.length !== options.passes))
|
|
144
|
+
throw new Error('Reused Jev capture must contain every case with the same pass count.');
|
|
145
|
+
}
|
|
135
146
|
if (typeof options.baselineLabel !== 'string' || !options.baselineLabel.trim() || options.baselineLabel.length > 200
|
|
136
147
|
|| prior && prior.baselineLabel !== options.baselineLabel)
|
|
137
148
|
throw new Error('Invalid or changed baseline label.');
|
|
@@ -147,7 +158,7 @@ export async function collectBenchmark(dataset, options) {
|
|
|
147
158
|
throw new Error('Extension cannot remove existing passes.');
|
|
148
159
|
const planned = cases.reduce((sum, c) => {
|
|
149
160
|
const row = rows.find(r => r.id === c.id);
|
|
150
|
-
return sum + (row ? 0 : 1) + Math.max(0, options.passes - (row?.jev.length ?? 0));
|
|
161
|
+
return sum + (row ? 0 : 1) + (reused ? 0 : Math.max(0, options.passes - (row?.jev.length ?? 0)));
|
|
151
162
|
}, 0);
|
|
152
163
|
if (planned > options.maxCalls)
|
|
153
164
|
throw new Error(`Insufficient maxCalls: need ${planned}; no providers invoked.`);
|
|
@@ -177,7 +188,7 @@ export async function collectBenchmark(dataset, options) {
|
|
|
177
188
|
return capture;
|
|
178
189
|
}
|
|
179
190
|
const environment = options.env ?? process.env;
|
|
180
|
-
if (!options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
|
|
191
|
+
if (!reused && !options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
|
|
181
192
|
&& !(environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim()))
|
|
182
193
|
throw new Error('Live benchmark requires JEV_API_KEY or AIWG_DECISION_JEV_API_KEY in the caller environment.');
|
|
183
194
|
const jev = options.jev ?? (async (input) => {
|
|
@@ -201,8 +212,9 @@ export async function collectBenchmark(dataset, options) {
|
|
|
201
212
|
if (!validObservation(sample) || !modeAnswer(c, sample))
|
|
202
213
|
throw new Error(`Invalid provider observation: ${c.id}`);
|
|
203
214
|
const price = (baseline ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
sample.cost = result.cost?.source === 'provider-reported' && validCost(result)
|
|
216
|
+
? structuredClone(result.cost) : { usd: price ? tokenCost(sample.usage, price) : null, price: structuredClone(price) };
|
|
217
|
+
const usd = sample.cost.usd;
|
|
206
218
|
if (usd === null)
|
|
207
219
|
stopReason = 'unknown-cost';
|
|
208
220
|
else {
|
|
@@ -215,7 +227,8 @@ export async function collectBenchmark(dataset, options) {
|
|
|
215
227
|
for (const c of cases) {
|
|
216
228
|
let row = rows.find(r => r.id === c.id);
|
|
217
229
|
if (!row) {
|
|
218
|
-
row = { id: c.id, baseline: await invoke(c, true),
|
|
230
|
+
row = { id: c.id, baseline: await invoke(c, true),
|
|
231
|
+
jev: structuredClone(reused?.rows.find(r => r.id === c.id).jev ?? []) };
|
|
219
232
|
rows.push(row);
|
|
220
233
|
}
|
|
221
234
|
if (options.onCheckpoint)
|
|
@@ -4,12 +4,12 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { askDecision, setupJev, DecisionAskUsageError, readDecisionContextFile, validateDecisionAskInput, } from './ask.js';
|
|
7
|
-
import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
|
|
7
|
+
import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, rotateDecisionLedger, parseDecisionLedgerRetention, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
|
|
8
8
|
export { DEFAULT_DECISION_SAVINGS_WEIGHTS, decisionSavingsWeights } from './savings.js';
|
|
9
9
|
import { resolveActiveProvider } from '../cli/provider-resolution.js';
|
|
10
10
|
export { appendDecisionOpportunity, buildDecisionOffloadSnapshot, decisionOffloadCategory, DECISION_OFFLOAD_CHECKPOINT_COVERAGE, DECISION_OFFLOAD_EXECUTABLE_COVERAGE, DECISION_OFFLOAD_SNAPSHOT_SCHEMA, DECISION_OPPORTUNITY_SCHEMA, runDecisionOffloadCheckpoint, } from './offload.js';
|
|
11
11
|
import { assertDecisionEvaluateDispatcherConfig, decisionPatternPacks, getDecisionPatternPack, governedPatternArtifacts, listDecisionPatterns, patternInputSchema, patternOutputSchema, planLiveDecisionPattern, runOfflineDecisionPattern, validateAgainstSchema, validateDecisionDocument, validateDefinition, validateRuleset, } from './index.js';
|
|
12
|
-
export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
|
|
12
|
+
export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, rotateDecisionLedger, parseDecisionLedgerRetention, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
|
|
13
13
|
const DECISION_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
14
|
const VALIDATION_TARGETS = ['request', 'definition', 'ruleset', 'binding'];
|
|
15
15
|
const OFFLINE_ADAPTER = 'agentic/code/addons/decision-engine/examples/fixture-jev-adapter.mjs';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolveDecisionEnablement } from '../config/decision-config.js';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { access, chmod,
|
|
3
|
+
import { access, chmod, lstat, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { JevCredentialError } from './adapters/jev.js';
|
|
8
|
+
import { readPrivateValueFile } from '../auth/sensitive-input.js';
|
|
8
9
|
export const JEV_CREDENTIAL_REF = 'typesafe-api';
|
|
9
10
|
export const JEV_DEFAULT_REGION = 'us';
|
|
10
11
|
function env(options) {
|
|
@@ -45,10 +46,22 @@ function parseRecord(raw) {
|
|
|
45
46
|
async function readCredentialFile(options = {}) {
|
|
46
47
|
const credentialPath = jevCredentialPath(options);
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Preserve the missing-file result and the existing directory requirement.
|
|
50
|
+
// The shared reader validates the opened descriptor, including ownership
|
|
51
|
+
// and link checks, before any credential record is parsed.
|
|
52
|
+
const [, parentStat] = await Promise.all([lstat(credentialPath), stat(path.dirname(credentialPath))]);
|
|
53
|
+
if (isInsecureMode(parentStat.mode)) {
|
|
54
|
+
throw Object.assign(new JevCredentialError('denied'), { message: 'Jev credential directory must not allow group or other access (chmod 700).' });
|
|
55
|
+
}
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readPrivateValueFile(credentialPath, 'Jev credential file');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Shared private-file diagnostics contain no path or credential bytes.
|
|
62
|
+
throw Object.assign(new JevCredentialError('denied'), { message: error.message });
|
|
63
|
+
}
|
|
64
|
+
return parseRecord(raw);
|
|
52
65
|
}
|
|
53
66
|
catch (error) {
|
|
54
67
|
if (error.code === 'ENOENT')
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* assumption that the artifact root is owned and written by the SAME local
|
|
22
22
|
* user running `aiwg`. It does not and cannot defend against a HOSTILE
|
|
23
23
|
* process running as that same user: such a process already has read/write
|
|
24
|
-
* access to everything the ledger could protect.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* access to everything the ledger could protect. Appenders, rotation and
|
|
25
|
+
* readers share the existing cross-process directory-lock helper. There is
|
|
26
|
+
* no privilege boundary here. Every filesystem
|
|
27
27
|
* check below uses only portable Node primitives (no `/proc`, no handle
|
|
28
28
|
* anchoring): `mkdir`/`lstat` for the directory, `open`+`fstat` for the
|
|
29
29
|
* file, with `O_NOFOLLOW` applied when the platform defines it (POSIX; not
|
|
@@ -39,25 +39,26 @@
|
|
|
39
39
|
* single-write atomicity boundary most platforms also honor for `PIPE_BUF`
|
|
40
40
|
* (4096 bytes on Linux): this makes a single, non-interleaved write the
|
|
41
41
|
* overwhelmingly likely outcome for one record, but it is not a guarantee.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* The lock covers opening, checking the trailing newline, writing every
|
|
43
|
+
* byte and closing. Rotation holds that same lock through live-file
|
|
44
|
+
* replacement. `history`/`stats` count malformed lines as skipped; retention
|
|
45
|
+
* preserves their original bytes.
|
|
46
46
|
*
|
|
47
47
|
* Recording — attribution resolution and the write — is bounded by a short
|
|
48
48
|
* deadline (`DECISION_LEDGER_RECORD_TIMEOUT_MS`). On timeout, the in-flight
|
|
49
49
|
* attempt is not cancelled (Node has no way to abort an in-flight `open`),
|
|
50
50
|
* so its outcome is genuinely unknown: the record may or may not eventually
|
|
51
51
|
* land. The warning says exactly that ("completion unknown"), and no
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* itself delay the subsequent write past the deadline.
|
|
52
|
+
* attribution-to-append stage starts once the deadline has fired. An append
|
|
53
|
+
* already waiting for the ledger lock can still complete after that deadline.
|
|
55
54
|
*/
|
|
56
55
|
import { randomUUID, createHash } from 'node:crypto';
|
|
57
56
|
import { constants as fsConstants, createReadStream, existsSync } from 'node:fs';
|
|
58
|
-
import { lstat, mkdir, open } from 'node:fs/promises';
|
|
57
|
+
import { lstat, mkdir, open, readdir, rename, rm } from 'node:fs/promises';
|
|
59
58
|
import { createInterface } from 'node:readline';
|
|
60
|
-
import { join } from 'node:path';
|
|
59
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
60
|
+
import { acquireDirectoryLock } from '../artifacts/prebuilt-build-lock.js';
|
|
61
|
+
import { syncDirectory } from '../storage/protected-files.js';
|
|
61
62
|
import { projectAiwgPath, projectAiwgWritePath } from '../config/project-artifacts.js';
|
|
62
63
|
import { redactText } from '../governance/redaction.js';
|
|
63
64
|
import { CanonicalJsonByteEstimator } from './context-plan.js';
|
|
@@ -121,6 +122,7 @@ export const MAX_LEDGER_RECORD_BYTES = 4000;
|
|
|
121
122
|
export const DECISION_LEDGER_RECORD_TIMEOUT_MS = 250;
|
|
122
123
|
const LEDGER_DIR_MODE = 0o700;
|
|
123
124
|
const LEDGER_FILE_MODE = 0o600;
|
|
125
|
+
const LEDGER_LOCK_TIMEOUT_MS = 30_000;
|
|
124
126
|
const IS_WINDOWS = process.platform === 'win32';
|
|
125
127
|
const HAS_O_NOFOLLOW = typeof fsConstants.O_NOFOLLOW === 'number';
|
|
126
128
|
function errorMessage(error) {
|
|
@@ -349,8 +351,8 @@ async function ensurePrivateDecisionDir(projectRoot, warn) {
|
|
|
349
351
|
return decisionDir;
|
|
350
352
|
}
|
|
351
353
|
/**
|
|
352
|
-
* Open
|
|
353
|
-
*
|
|
354
|
+
* Open a private ledger/archive file (append by default, read-only for
|
|
355
|
+
* retention) and verify it is a regular file with exactly one hard link (rejects a
|
|
354
356
|
* pre-planted hard link to an unrelated file, which `O_NOFOLLOW` alone does
|
|
355
357
|
* not catch), and on POSIX owned by the current user with no group/other
|
|
356
358
|
* permission bits. `O_NOFOLLOW` is applied when the platform defines it
|
|
@@ -360,7 +362,7 @@ async function ensurePrivateDecisionDir(projectRoot, warn) {
|
|
|
360
362
|
* check is closed untouched (no chmod, no write) and recording is skipped
|
|
361
363
|
* with exactly one warning naming the reason.
|
|
362
364
|
*/
|
|
363
|
-
async function
|
|
365
|
+
async function openPrivateLedgerFile(ledgerPath, warn, flags = fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_WRONLY) {
|
|
364
366
|
try {
|
|
365
367
|
const pre = await lstat(ledgerPath);
|
|
366
368
|
if (pre.isSymbolicLink()) {
|
|
@@ -374,10 +376,9 @@ async function openLedgerFileForAppend(ledgerPath, warn) {
|
|
|
374
376
|
return null;
|
|
375
377
|
}
|
|
376
378
|
}
|
|
377
|
-
const flags = fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_WRONLY | (HAS_O_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
378
379
|
let handle;
|
|
379
380
|
try {
|
|
380
|
-
handle = await open(ledgerPath, flags, LEDGER_FILE_MODE);
|
|
381
|
+
handle = await open(ledgerPath, flags | (HAS_O_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0), LEDGER_FILE_MODE);
|
|
381
382
|
}
|
|
382
383
|
catch (error) {
|
|
383
384
|
warn(`warning: cannot open decision ledger file: ${errorMessage(error)}`);
|
|
@@ -466,10 +467,13 @@ export async function appendDecisionLedgerRecord(record, options) {
|
|
|
466
467
|
if (!decisionDir)
|
|
467
468
|
return false;
|
|
468
469
|
const ledgerPath = join(decisionDir, DECISION_LEDGER_FILE_NAME);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return false;
|
|
470
|
+
let release;
|
|
471
|
+
let handle = null;
|
|
472
472
|
try {
|
|
473
|
+
release = await lockDecisionLedger(decisionDir);
|
|
474
|
+
handle = await openPrivateLedgerFile(ledgerPath, warn);
|
|
475
|
+
if (!handle)
|
|
476
|
+
return false;
|
|
473
477
|
const prefix = (await needsLeadingNewline(ledgerPath)) ? '\n' : '';
|
|
474
478
|
await writeFully(handle, Buffer.from(prefix + line, 'utf8'), options.writeChunk);
|
|
475
479
|
return true;
|
|
@@ -479,7 +483,162 @@ export async function appendDecisionLedgerRecord(record, options) {
|
|
|
479
483
|
return false;
|
|
480
484
|
}
|
|
481
485
|
finally {
|
|
482
|
-
await handle
|
|
486
|
+
await handle?.close().catch(() => { });
|
|
487
|
+
await release?.().catch(error => warn(`warning: cannot release decision ledger lock: ${errorMessage(error)}`));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function lockDecisionLedger(decisionDir) {
|
|
491
|
+
return acquireDirectoryLock(join(decisionDir, 'ledger.lock'), {
|
|
492
|
+
timeoutMs: LEDGER_LOCK_TIMEOUT_MS, pollMs: 10, reclaimDeadOwner: true,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
export function parseDecisionLedgerRetention(keepLast, all = false) {
|
|
496
|
+
if (all) {
|
|
497
|
+
if (keepLast !== undefined)
|
|
498
|
+
throw new Error('--all cannot be combined with --keep-last');
|
|
499
|
+
return {};
|
|
500
|
+
}
|
|
501
|
+
const match = /^(\d+)(d?)$/.exec(keepLast ?? '90d');
|
|
502
|
+
const n = match ? Number(match[1]) : NaN;
|
|
503
|
+
if (!Number.isSafeInteger(n) || n <= 0 || !Number.isSafeInteger(n * (match?.[2] ? 86_400_000 : 1))) {
|
|
504
|
+
throw new Error('--keep-last must be a positive count or <N>d; rotating everything requires --all');
|
|
505
|
+
}
|
|
506
|
+
return match?.[2] ? { ms: n * 86_400_000 } : { count: n };
|
|
507
|
+
}
|
|
508
|
+
function isBeneath(root, target) {
|
|
509
|
+
const rel = relative(root, target);
|
|
510
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
511
|
+
}
|
|
512
|
+
/** Walk before mkdir: recursive mkdir alone could write through a symlinked ancestor. */
|
|
513
|
+
async function privateArchiveDirectory(root, target, create) {
|
|
514
|
+
let current = root;
|
|
515
|
+
for (const part of relative(root, target).split(sep)) {
|
|
516
|
+
current = join(current, part);
|
|
517
|
+
if (create) {
|
|
518
|
+
try {
|
|
519
|
+
await mkdir(current, { mode: LEDGER_DIR_MODE });
|
|
520
|
+
if (!IS_WINDOWS)
|
|
521
|
+
await syncDirectory(dirname(current));
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (error.code !== 'EEXIST')
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
let info;
|
|
529
|
+
try {
|
|
530
|
+
info = await lstat(current);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (!create && error.code === 'ENOENT')
|
|
534
|
+
return false;
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
// archive/ is shared with the activity log and may already be readable.
|
|
538
|
+
// Its decision/ subtree (and any nested directories) must be private.
|
|
539
|
+
const modeMask = current === join(root, 'archive') ? 0o022 : 0o077;
|
|
540
|
+
if (!info.isDirectory() || (!IS_WINDOWS && (info.uid !== currentUid() || (info.mode & modeMask) !== 0))) {
|
|
541
|
+
throw new Error(`Unsafe decision archive directory (requires a private, owned real directory): ${current}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
/** Retention preserves each raw line, including invalid UTF-8, CRLF and an unterminated final line. */
|
|
547
|
+
export async function rotateDecisionLedger(options) {
|
|
548
|
+
const retention = parseDecisionLedgerRetention(options.keepLast, options.all);
|
|
549
|
+
const now = (options.now ?? Date.now)();
|
|
550
|
+
const artifactRoot = projectAiwgWritePath(options.projectRoot);
|
|
551
|
+
const archiveRoot = join(artifactRoot, 'archive', 'decision');
|
|
552
|
+
const archivePath = resolve(artifactRoot, options.to ?? join('archive', 'decision', `${new Date(now).toISOString().replace(/[-:]/g, '').replace('T', '-').replace(/\.\d{3}Z$/, '')}-${randomUUID()}.jsonl`));
|
|
553
|
+
if (!isBeneath(artifactRoot, archivePath))
|
|
554
|
+
throw new Error('Archive path must stay inside the resolved artifact root');
|
|
555
|
+
if (!isBeneath(archiveRoot, archivePath) || !archivePath.endsWith('.jsonl')) {
|
|
556
|
+
throw new Error('Archive path must be a .jsonl file beneath archive/decision/ in the resolved artifact root');
|
|
557
|
+
}
|
|
558
|
+
const fail = (message) => { throw new Error(message); };
|
|
559
|
+
const decisionDir = await ensurePrivateDecisionDir(options.projectRoot, fail);
|
|
560
|
+
if (!decisionDir)
|
|
561
|
+
throw new Error('Cannot access decision ledger directory');
|
|
562
|
+
const release = await lockDecisionLedger(decisionDir);
|
|
563
|
+
const ledgerPath = join(decisionDir, DECISION_LEDGER_FILE_NAME);
|
|
564
|
+
const tempPath = join(decisionDir, `.ledger-${randomUUID()}.tmp`);
|
|
565
|
+
let archiveCreated = false;
|
|
566
|
+
try {
|
|
567
|
+
const live = await openPrivateLedgerFile(ledgerPath, fail, fsConstants.O_RDONLY | fsConstants.O_CREAT);
|
|
568
|
+
if (!live)
|
|
569
|
+
throw new Error('Cannot read decision ledger');
|
|
570
|
+
let content;
|
|
571
|
+
try {
|
|
572
|
+
content = await live.readFile();
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
await live.close();
|
|
576
|
+
}
|
|
577
|
+
const lines = [];
|
|
578
|
+
for (let start = 0; start < content.length;) {
|
|
579
|
+
const newline = content.indexOf(0x0a, start);
|
|
580
|
+
const end = newline < 0 ? content.length : newline + 1;
|
|
581
|
+
const bytes = content.subarray(start, end);
|
|
582
|
+
let timestamp = null;
|
|
583
|
+
try {
|
|
584
|
+
const record = JSON.parse(bytes.toString('utf8'));
|
|
585
|
+
if (isDecisionLedgerRecord(record))
|
|
586
|
+
timestamp = Date.parse(record.timestamp);
|
|
587
|
+
}
|
|
588
|
+
catch { /* preserve malformed bytes */ }
|
|
589
|
+
lines.push({ bytes, timestamp });
|
|
590
|
+
start = end;
|
|
591
|
+
}
|
|
592
|
+
const valid = lines.filter(line => line.timestamp !== null).sort((a, b) => a.timestamp - b.timestamp);
|
|
593
|
+
const recent = new Set(retention.count === undefined ? [] : valid.slice(-retention.count));
|
|
594
|
+
const kept = [], archived = [];
|
|
595
|
+
for (const line of lines) {
|
|
596
|
+
const keep = !options.all && (line.timestamp === null || (retention.count !== undefined
|
|
597
|
+
? recent.has(line) : line.timestamp >= now - retention.ms));
|
|
598
|
+
(keep ? kept : archived).push(line.bytes);
|
|
599
|
+
}
|
|
600
|
+
const result = { archived: archived.length, retained: kept.length,
|
|
601
|
+
malformed: lines.length - valid.length, archivePath: archived.length ? archivePath : null };
|
|
602
|
+
if (!archived.length)
|
|
603
|
+
return result;
|
|
604
|
+
await privateArchiveDirectory(artifactRoot, dirname(archivePath), true);
|
|
605
|
+
const archive = await open(archivePath, 'wx', LEDGER_FILE_MODE);
|
|
606
|
+
archiveCreated = true;
|
|
607
|
+
try {
|
|
608
|
+
await writeFully(archive, Buffer.concat(archived));
|
|
609
|
+
await archive.sync();
|
|
610
|
+
}
|
|
611
|
+
finally {
|
|
612
|
+
await archive.close();
|
|
613
|
+
}
|
|
614
|
+
if (!IS_WINDOWS)
|
|
615
|
+
await syncDirectory(dirname(archivePath));
|
|
616
|
+
const replacement = await open(tempPath, 'wx', LEDGER_FILE_MODE);
|
|
617
|
+
try {
|
|
618
|
+
await writeFully(replacement, Buffer.concat(kept));
|
|
619
|
+
await replacement.sync();
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
await replacement.close();
|
|
623
|
+
}
|
|
624
|
+
await rename(tempPath, ledgerPath);
|
|
625
|
+
archiveCreated = false; // Both files now own their respective records.
|
|
626
|
+
if (!IS_WINDOWS)
|
|
627
|
+
await syncDirectory(decisionDir);
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
if (archiveCreated)
|
|
632
|
+
await rm(archivePath, { force: true }); // Live file is still intact.
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
try {
|
|
637
|
+
await rm(tempPath, { force: true });
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
await release();
|
|
641
|
+
}
|
|
483
642
|
}
|
|
484
643
|
}
|
|
485
644
|
/** Never throws: a provider-detection failure (missing config, unreadable capability matrix, …) degrades to "unknown". */
|
|
@@ -723,20 +882,57 @@ async function* streamDecisionLedgerLines(path) {
|
|
|
723
882
|
rl.close();
|
|
724
883
|
}
|
|
725
884
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
885
|
+
async function archiveLedgerPaths(artifactRoot, directory = join(artifactRoot, 'archive', 'decision')) {
|
|
886
|
+
if (!await privateArchiveDirectory(artifactRoot, directory, false))
|
|
887
|
+
return [];
|
|
888
|
+
const paths = [];
|
|
889
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
890
|
+
const target = join(directory, entry.name);
|
|
891
|
+
if (entry.isSymbolicLink())
|
|
892
|
+
throw new Error(`Unsafe decision archive symlink: ${target}`);
|
|
893
|
+
if (entry.isDirectory())
|
|
894
|
+
paths.push(...await archiveLedgerPaths(artifactRoot, target));
|
|
895
|
+
else if (entry.name.endsWith('.jsonl')) {
|
|
896
|
+
const handle = await openPrivateLedgerFile(target, message => { throw new Error(message); }, fsConstants.O_RDONLY);
|
|
897
|
+
if (!handle)
|
|
898
|
+
throw new Error(`Unsafe decision archive: ${target}`);
|
|
899
|
+
await handle.close();
|
|
900
|
+
paths.push(target);
|
|
735
901
|
}
|
|
736
|
-
|
|
737
|
-
|
|
902
|
+
}
|
|
903
|
+
return paths.sort();
|
|
904
|
+
}
|
|
905
|
+
/** Streams validated records under the rotation lock; invalid lines yield null. */
|
|
906
|
+
async function* streamDecisionLedgerRecords(projectRoot, includeArchives = false) {
|
|
907
|
+
const livePath = ledgerReadPath(projectRoot);
|
|
908
|
+
const decisionDir = dirname(livePath);
|
|
909
|
+
if (!existsSync(decisionDir))
|
|
910
|
+
return;
|
|
911
|
+
if (!(await lstat(decisionDir)).isDirectory())
|
|
912
|
+
throw new Error('Decision ledger directory must be a real directory');
|
|
913
|
+
// A single lock also prevents readers from observing the archive and live
|
|
914
|
+
// replacement at different points in a rotation (duplicates or omissions).
|
|
915
|
+
const release = await lockDecisionLedger(decisionDir);
|
|
916
|
+
try {
|
|
917
|
+
const paths = includeArchives ? [...await archiveLedgerPaths(projectAiwgPath(projectRoot)), livePath] : [livePath];
|
|
918
|
+
for (const path of paths) {
|
|
919
|
+
for await (const line of streamDecisionLedgerLines(path)) {
|
|
920
|
+
const trimmed = line.trim();
|
|
921
|
+
if (!trimmed)
|
|
922
|
+
continue;
|
|
923
|
+
try {
|
|
924
|
+
const parsed = JSON.parse(trimmed);
|
|
925
|
+
yield isDecisionLedgerRecord(parsed) ? parsed : null;
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
yield null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
738
931
|
}
|
|
739
932
|
}
|
|
933
|
+
finally {
|
|
934
|
+
await release();
|
|
935
|
+
}
|
|
740
936
|
}
|
|
741
937
|
function matchesFilters(record, filters) {
|
|
742
938
|
if (filters.since && new Date(record.timestamp).getTime() < filters.since.getTime())
|
|
@@ -765,7 +961,7 @@ export function filterDecisionLedgerRecords(records, filters) {
|
|
|
765
961
|
export async function decisionLedgerHistory(projectRoot, filters = {}) {
|
|
766
962
|
const records = [];
|
|
767
963
|
let skipped = 0;
|
|
768
|
-
for await (const record of streamDecisionLedgerRecords(projectRoot)) {
|
|
964
|
+
for await (const record of streamDecisionLedgerRecords(projectRoot, filters.includeArchives)) {
|
|
769
965
|
if (!record) {
|
|
770
966
|
skipped += 1;
|
|
771
967
|
continue;
|
|
@@ -897,7 +1093,7 @@ export async function decisionLedgerStats(projectRoot, filters = {}) {
|
|
|
897
1093
|
const weights = decisionSavingsWeights(config?.decision?.ask);
|
|
898
1094
|
const groups = new Map();
|
|
899
1095
|
let skipped = 0;
|
|
900
|
-
for await (const record of streamDecisionLedgerRecords(projectRoot)) {
|
|
1096
|
+
for await (const record of streamDecisionLedgerRecords(projectRoot, filters.includeArchives)) {
|
|
901
1097
|
if (!record) {
|
|
902
1098
|
skipped += 1;
|
|
903
1099
|
continue;
|
|
@@ -4,7 +4,7 @@ import { matchEvidenceDigest } from '../../gates/stats/index.js';
|
|
|
4
4
|
import { CalibrationRegistry, calibrationArtifactDigest } from '../calibration/registry.js';
|
|
5
5
|
import { validateCaseInventory } from './manifest.js';
|
|
6
6
|
import { verifyQualificationSplits } from './quality.js';
|
|
7
|
-
import { verifyDecisionStagedLoadRecord } from './load.js';
|
|
7
|
+
import { DECISION_STAGED_LOAD_RESULT_VERSIONS, verifyDecisionStagedLoadRecord } from './load.js';
|
|
8
8
|
import { validateReviewEvaluation } from '../review/validate.js';
|
|
9
9
|
const range = (prefix, from, to) => Array.from({ length: to - from + 1 }, (_, index) => `${prefix}${String(from + index).padStart(2, '0')}`);
|
|
10
10
|
export const DECISION_GATE_SUITES = {
|
|
@@ -34,7 +34,7 @@ export const DECISION_GATE_SUITES = {
|
|
|
34
34
|
export const GATE_ARTIFACT_SCHEMAS = {
|
|
35
35
|
'immutable-splits': 'decision-binary-benchmark-plan/v2',
|
|
36
36
|
'calibration-qualified': 'decision-calibration-artifact/v1',
|
|
37
|
-
'load-manifest-qualified': 'decision-load-result/
|
|
37
|
+
'load-manifest-qualified': 'decision-load-result/v2',
|
|
38
38
|
'review-decision-recorded': 'decision-qualification-review/v1',
|
|
39
39
|
};
|
|
40
40
|
export const GATE_ARTIFACT_FLAGS = Object.keys(GATE_ARTIFACT_SCHEMAS);
|
|
@@ -127,7 +127,7 @@ function validCalibration(value, context) {
|
|
|
127
127
|
&& artifact.metrics.selectiveRisk <= artifact.profile.maximumSelectiveRisk;
|
|
128
128
|
}
|
|
129
129
|
function validLoadResult(value, context) {
|
|
130
|
-
if (!record(value) || value.schemaVersion
|
|
130
|
+
if (!record(value) || !DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(String(value.schemaVersion)))
|
|
131
131
|
return false;
|
|
132
132
|
// Legacy records remain readable, but only completed live v3 evidence bound to
|
|
133
133
|
// this exact clean source revision can qualify the performance gate.
|
|
@@ -177,7 +177,9 @@ export function deriveGateEvidence(manifest, proof = {}) {
|
|
|
177
177
|
const ref = manifest.gateArtifacts?.[flag];
|
|
178
178
|
const versionOk = flag === 'immutable-splits'
|
|
179
179
|
? ref !== undefined && IMMUTABLE_SPLIT_VERSIONS.includes(ref.schemaVersion)
|
|
180
|
-
:
|
|
180
|
+
: flag === 'load-manifest-qualified'
|
|
181
|
+
? ref !== undefined && DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(ref.schemaVersion)
|
|
182
|
+
: ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
|
|
181
183
|
flags[flag] = versionOk && ref !== undefined
|
|
182
184
|
&& typeof ref.artifact === 'string' && ref.artifact.length > 0 && sha256Pattern.test(ref.digest);
|
|
183
185
|
if (flag === 'load-manifest-qualified') {
|