@aiwg/cli 2026.10.11 → 2026.10.12
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/agentic/code/providers/model-catalog.v1.json +62 -12
- package/dist/src/artifacts/binding-target-identity.js +51 -0
- package/dist/src/artifacts/checksum-manifest.js +12 -0
- package/dist/src/artifacts/index-builder.js +11 -13
- package/dist/src/artifacts/query-engine.js +5 -11
- package/dist/src/cli/handlers/doctor-help.js +32 -0
- package/dist/src/cli/handlers/gates.js +19 -18
- package/dist/src/cli/handlers/models-help.js +7 -1
- package/dist/src/cli/handlers/models.js +3 -3
- package/dist/src/cli/handlers/script-runner.js +8 -5
- package/dist/src/cli/handlers/sessions-intake.js +104 -0
- package/dist/src/cli/handlers/sessions-tail.js +172 -0
- package/dist/src/cli/handlers/sessions.js +90 -17
- package/dist/src/cli/handlers/skill-lint.js +10 -3
- package/dist/src/cli/handlers/steward-format.js +100 -0
- package/dist/src/cli/handlers/steward-models.js +167 -0
- package/dist/src/cli/handlers/steward.js +14 -248
- package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
- package/dist/src/cli/handlers/use.js +6 -24
- package/dist/src/cli/handlers/utilities.js +1 -24
- package/dist/src/config/aiwg-config.js +7 -0
- package/dist/src/config/authorization-cli.js +89 -0
- package/dist/src/config/cli-help.js +1 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/decision/context-pruning.js +3 -2
- package/dist/src/decision/ensemble/contract.js +15 -4
- package/dist/src/decision/ensemble-study/artifacts.js +5 -0
- package/dist/src/decision/ensemble-study/score.js +4 -3
- package/dist/src/decision/feature-export.js +3 -9
- package/dist/src/decision/heldout/collector.js +2 -2
- package/dist/src/decision/issue-triage/runtime.js +3 -9
- package/dist/src/decision/qualification/gates.js +2 -1
- package/dist/src/decision/qualification/release.js +2 -3
- package/dist/src/decision/routing/report.js +2 -0
- package/dist/src/decision/sdlc-screening.js +3 -5
- package/dist/src/decision/sensitivity/comparative.js +5 -3
- package/dist/src/gates/driver.js +6 -77
- package/dist/src/gates/evaluate.js +4 -11
- package/dist/src/gates/floors.js +4 -4
- package/dist/src/gates/index.js +3 -3
- package/dist/src/gates/lattice.js +26 -0
- package/dist/src/gates/registry.js +2 -1
- package/dist/src/gates/report.js +19 -7
- package/dist/src/issues/audit-cli.js +120 -0
- package/dist/src/issues/audit-contract.js +64 -0
- package/dist/src/issues/audit-local.js +82 -0
- package/dist/src/issues/audit.js +220 -0
- package/dist/src/lint/rule-activation.js +15 -1
- package/dist/src/mission-protocol/canonical-codecs.js +270 -0
- package/dist/src/mission-protocol/codecs.js +22 -550
- package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
- package/dist/src/mission-protocol/health-trace-types.js +43 -0
- package/dist/src/mission-protocol/types.js +2 -42
- package/dist/src/mission-protocol/worker-contract-json.js +10 -0
- package/dist/src/mission-protocol/worker-review-gate.js +100 -0
- package/dist/src/mission-protocol/worker-review-types.js +2 -0
- package/dist/src/models/catalog-policy.js +280 -0
- package/dist/src/models/index.js +1 -1
- package/dist/src/models/model-catalog.v1.json +62 -12
- package/dist/src/models/policy-resources.js +28 -0
- package/dist/src/models/provider-policy.js +10 -546
- package/dist/src/models/resolver.js +26 -8
- package/dist/src/models/router.js +15 -2
- package/dist/src/models/tiers.js +13 -0
- package/dist/src/models/types.js +0 -6
- package/dist/src/models/worker-ceilings.js +59 -0
- package/dist/src/models/worker-invocation-policy.js +243 -0
- package/dist/src/models/worker-plan.js +16 -20
- package/dist/src/models/worker-review-lineage.js +55 -0
- package/dist/src/models/worker-selection-policy.js +1 -1
- package/dist/src/models/wrapper-route.js +6 -3
- package/dist/src/policy/authorization.js +1 -0
- package/dist/src/policy/hitl-config.js +63 -0
- package/dist/src/policy/hitl-receipt.js +57 -0
- package/dist/src/policy/hitl-types.js +4 -0
- package/dist/src/policy/hitl-validation.js +39 -0
- package/dist/src/policy/hitl.js +117 -0
- package/dist/src/rules/activation.d.mts +1 -1
- package/dist/src/rules/activation.mjs +0 -2
- package/dist/src/rules/gate-coverage.js +51 -0
- package/dist/src/rules/gate-references.js +16 -0
- package/dist/src/rules/rule-frontmatter.schema.json +7 -1
- package/dist/src/serve/worker-command-request.js +21 -4
- package/dist/src/serve/worker-invocation-preparation.js +3 -1
- package/dist/src/serve/worker-invocation.js +21 -10
- package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
- package/dist/src/sessions/adapters/grok-build-native.js +269 -0
- package/dist/src/sessions/adapters/grok-build.js +57 -15
- package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
- package/dist/src/sessions/adapters/hermes.js +42 -3
- package/dist/src/sessions/adapters/warp.js +1 -1
- package/dist/src/sessions/batch-import.js +10 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/discovery-capability.js +38 -0
- package/dist/src/sessions/importer.js +8 -251
- package/dist/src/sessions/index.js +4 -0
- package/dist/src/sessions/intake-dir.js +71 -0
- package/dist/src/sessions/normalization.js +251 -0
- package/dist/src/sessions/tail.js +91 -0
- package/dist/src/sessions/workspace-discovery.js +83 -7
- package/dist/src/skills/agent-skill-sidecar.js +28 -0
- package/dist/src/skills/agent-skills.js +4 -23
- package/dist/src/skills/claude-surface-projection.js +25 -0
- package/dist/src/skills/deployer.js +8 -288
- package/dist/src/skills/deployment-policy.js +114 -0
- package/dist/src/skills/deployment-projection.js +152 -0
- package/dist/src/skills/deployment-types.js +20 -0
- package/dist/src/skills/exporter.js +8 -31
- package/dist/src/skills/provider-extensions.js +41 -0
- package/dist/src/skills/run.js +6 -0
- package/dist/src/skills/types.js +0 -8
- package/dist/src/skills/validator-resources.js +68 -0
- package/dist/src/skills/validator.js +6 -55
- package/dist/src/storage/cli-migrate-options.js +125 -0
- package/dist/src/storage/cli-migrate.js +111 -0
- package/dist/src/storage/cli.js +4 -279
- package/dist/src/storage/migration-adapter.js +79 -0
- package/dist/src/storage/migration-copy.js +108 -0
- package/dist/src/storage/migration-protocol.js +14 -197
- package/dist/src/storage/migration-records.js +186 -0
- package/dist/src/storage/migration-types.js +12 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/tracker/issue-pages.js +110 -0
- package/package.json +1 -1
- package/schemas/mission-v1.schema.json +335 -0
- package/schemas/models/model-catalog.v1.schema.json +19 -1
- package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
- package/schemas/models/model-policy.v1.schema.json +1 -1
- package/tools/agents/providers/provider-paths-staging.mjs +35 -0
- package/tools/agents/providers/rule-binding-targets.mjs +146 -0
- package/tools/agents/providers/rule-delivery.mjs +7 -126
- package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateHitlPolicy } from '../policy/hitl-validation.js';
|
|
1
2
|
import { normalizeAiwgConfigCandidate } from './config-normalization.js';
|
|
2
3
|
import { atomicConfigWrite, withWorkerWriteLock } from './worker-write.js';
|
|
3
4
|
import { workerConfigDigest } from './worker-config-digest.js';
|
|
@@ -979,6 +980,9 @@ export async function readAiwgConfigCandidate(projectDir) {
|
|
|
979
980
|
if (workspaceErrors.length > 0) {
|
|
980
981
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${workspaceErrors.join('\n')}`);
|
|
981
982
|
}
|
|
983
|
+
const hitlErrors = validateHitlPolicy(parsed.authorization?.hitl);
|
|
984
|
+
if (hitlErrors.length)
|
|
985
|
+
throw new Error(hitlErrors.join("\n"));
|
|
982
986
|
const authorizationErrors = parsed.authorization
|
|
983
987
|
? validateAuthorization(parsed.authorization).filter(item => item.severity === 'error')
|
|
984
988
|
: [];
|
|
@@ -1050,6 +1054,9 @@ async function writeAiwgConfigUnlocked(projectDir, config) {
|
|
|
1050
1054
|
throw error;
|
|
1051
1055
|
}
|
|
1052
1056
|
}
|
|
1057
|
+
const hitlErrors = validateHitlPolicy(config.authorization?.hitl);
|
|
1058
|
+
if (hitlErrors.length)
|
|
1059
|
+
throw new Error(hitlErrors.join("\n"));
|
|
1053
1060
|
const pinConfigs = loadRulePinConfigs(projectDir, config);
|
|
1054
1061
|
const currentCapabilities = new Set(installedRuleCapabilities(pinConfigs));
|
|
1055
1062
|
const removed = previous ? installedRuleCapabilities({ ...pinConfigs, project: previous }).filter(id => !currentCapabilities.has(id)) : [];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Exposes HITL inspection, validation, preview, enable/revoke and signed receipts under config/steward.
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { AiwgError, EXIT_CODES } from '../cli/errors.js';
|
|
5
|
+
import { getProjectDir, readAiwgConfig, writeAiwgConfig } from './aiwg-config.js';
|
|
6
|
+
import { ensureAuthorization, loadHitlLayers } from '../policy/hitl-config.js';
|
|
7
|
+
import { resolveHitlAuthorization } from '../policy/hitl.js';
|
|
8
|
+
import { validateHitlPolicy } from '../policy/hitl-validation.js';
|
|
9
|
+
export const authorizationUsage = `aiwg config authorization (also aiwg steward authorization)
|
|
10
|
+
inspect | validate [--target DIRECTORY]
|
|
11
|
+
preview --action-file FILE [--target DIRECTORY]
|
|
12
|
+
enable --policy-file FILE [--target DIRECTORY]
|
|
13
|
+
revoke [--profile NAME] [--target DIRECTORY]
|
|
14
|
+
receipt --action-file FILE [--target DIRECTORY]
|
|
15
|
+
Preview is read-only. Receipt writes the decision to the existing signed effect ledger;
|
|
16
|
+
it does not perform the proposed action. Action files cannot supply session authorization.`;
|
|
17
|
+
const usageError = (message) => new AiwgError({ code: 'ERR_AUTHORIZATION_USAGE', message, exitCode: EXIT_CODES.USAGE });
|
|
18
|
+
async function jsonFile(file) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw usageError('Authorization JSON input could not be read or parsed');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function authorizationCli(args, cwd = process.cwd()) {
|
|
27
|
+
if (!args.length || args.includes('--help') || args[0] === 'help') {
|
|
28
|
+
console.log(authorizationUsage);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const [operation, ...rest] = args;
|
|
32
|
+
const flags = {};
|
|
33
|
+
const allowed = ['--target', ...(operation === 'enable' ? ['--policy-file'] : []),
|
|
34
|
+
...(['preview', 'receipt'].includes(operation) ? ['--action-file'] : []), ...(operation === 'revoke' ? ['--profile'] : [])];
|
|
35
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
36
|
+
if (!allowed.includes(rest[i]) || !rest[i + 1] || rest[i + 1].startsWith('--') || flags[rest[i]])
|
|
37
|
+
throw usageError('Invalid authorization option');
|
|
38
|
+
flags[rest[i]] = rest[i + 1];
|
|
39
|
+
}
|
|
40
|
+
const target = resolve(getProjectDir({ cwd }, rest));
|
|
41
|
+
if (operation === 'inspect' || operation === 'validate') {
|
|
42
|
+
const loaded = await loadHitlLayers(target);
|
|
43
|
+
console.log(JSON.stringify(operation === 'inspect' ? loaded : { valid: true, layers: loaded.layers.length }, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (operation === 'preview' || operation === 'receipt') {
|
|
47
|
+
if (!flags['--action-file'])
|
|
48
|
+
throw usageError('--action-file is required');
|
|
49
|
+
const action = await jsonFile(resolve(cwd, flags['--action-file']));
|
|
50
|
+
const loaded = await loadHitlLayers(target);
|
|
51
|
+
const context = { now: new Date().toISOString(), layers: loaded.layers };
|
|
52
|
+
if (operation === 'preview')
|
|
53
|
+
console.log(JSON.stringify(resolveHitlAuthorization(action, context), null, 2));
|
|
54
|
+
else {
|
|
55
|
+
const { effectSession, openLedger } = await import('../effects/host.js');
|
|
56
|
+
const { recordHitlAuthorization } = await import('../policy/hitl-receipt.js');
|
|
57
|
+
const session = await effectSession(loaded.projectDir, 'custom');
|
|
58
|
+
console.log(JSON.stringify(await recordHitlAuthorization(openLedger(session), action, context), null, 2));
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (operation !== 'enable' && operation !== 'revoke')
|
|
63
|
+
throw usageError('Unknown authorization operation');
|
|
64
|
+
const config = await readAiwgConfig(target);
|
|
65
|
+
if (!config)
|
|
66
|
+
throw usageError('No project config found; initialize project config first');
|
|
67
|
+
const authorization = ensureAuthorization(config);
|
|
68
|
+
if (operation === 'enable') {
|
|
69
|
+
if (!flags['--policy-file'])
|
|
70
|
+
throw usageError('--policy-file is required');
|
|
71
|
+
const policy = await jsonFile(resolve(cwd, flags['--policy-file']));
|
|
72
|
+
const errors = validateHitlPolicy(policy);
|
|
73
|
+
if (policy === undefined || errors.length)
|
|
74
|
+
throw usageError('Invalid authorization.hitl policy');
|
|
75
|
+
authorization.hitl = { ...policy, enabled: true };
|
|
76
|
+
}
|
|
77
|
+
else if (authorization.hitl) {
|
|
78
|
+
if (flags['--profile']) {
|
|
79
|
+
if (!Object.hasOwn(authorization.hitl.profiles, flags['--profile']))
|
|
80
|
+
throw usageError('Unknown authorization profile');
|
|
81
|
+
delete authorization.hitl.profiles[flags['--profile']];
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
authorization.hitl.enabled = false;
|
|
85
|
+
}
|
|
86
|
+
await writeAiwgConfig(target, config);
|
|
87
|
+
console.log(JSON.stringify({ operation, enabled: authorization.hitl?.enabled ?? false }));
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=authorization-cli.js.map
|
|
@@ -17,6 +17,7 @@ Subcommands:
|
|
|
17
17
|
path Print config directory path
|
|
18
18
|
edit Open config in $EDITOR/$VISUAL (requires a terminal)
|
|
19
19
|
edit --print-path Print the config file path instead of opening an editor
|
|
20
|
+
authorization Inspect/validate/preview/enable/revoke bounded HITL policy
|
|
20
21
|
gitignore Show/check/fix .gitignore AIWG entries
|
|
21
22
|
|
|
22
23
|
Reviewed worker policy pair (project scope, no prompts):
|
package/dist/src/config/cli.js
CHANGED
|
@@ -33,6 +33,11 @@ const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
33
33
|
* Main CLI entry point for `aiwg config <subcommand> [args]`
|
|
34
34
|
*/
|
|
35
35
|
export async function main(args) {
|
|
36
|
+
if (args[0] === 'authorization') {
|
|
37
|
+
const { authorizationCli } = await import('./authorization-cli.js');
|
|
38
|
+
await authorizationCli(args.slice(1));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
36
41
|
if (await handleWorkerAdoptionArgs(args, process.cwd(), 'config'))
|
|
37
42
|
return;
|
|
38
43
|
// Extract --config-dir flag before routing
|
|
@@ -291,7 +296,7 @@ async function projectConfigSet(key, raw, args, quiet = false) {
|
|
|
291
296
|
}
|
|
292
297
|
// Coerce booleans for known boolean fields
|
|
293
298
|
let value = raw;
|
|
294
|
-
if (key === 'rules' || key.startsWith('rules.') || key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
|
|
299
|
+
if (key === 'authorization' || key.startsWith('authorization.') || key === 'rules' || key.startsWith('rules.') || key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
|
|
295
300
|
try {
|
|
296
301
|
value = JSON.parse(raw);
|
|
297
302
|
}
|
|
@@ -397,6 +402,10 @@ async function projectConfigSet(key, raw, args, quiet = false) {
|
|
|
397
402
|
? (existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : emptyConfig())
|
|
398
403
|
: (await readAiwgConfig(projectDir)) ?? emptyConfig();
|
|
399
404
|
const before = structuredClone(getDottedPath(cfg, key));
|
|
405
|
+
if (key.startsWith('authorization.hitl')) {
|
|
406
|
+
const { ensureAuthorization } = await import('../policy/hitl-config.js');
|
|
407
|
+
ensureAuthorization(cfg);
|
|
408
|
+
}
|
|
400
409
|
setDottedPath(cfg, key, value);
|
|
401
410
|
const externalLinkErrors = validateExternalLinks(cfg.externalLinks);
|
|
402
411
|
if (externalLinkErrors.length > 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision } from '../gates/lattice.js';
|
|
1
2
|
import { ContextBudgetManager } from '../metrics/context-budget.js';
|
|
2
3
|
import { sha256 } from './compile-cache/identity.js';
|
|
3
4
|
import { admitEntry } from './entry.js';
|
|
@@ -436,9 +437,9 @@ export function buildContextPruningEvaluationReport(input) {
|
|
|
436
437
|
findings.add(`missing-input:${name}`);
|
|
437
438
|
// Automatic rollback triggers from the D26 rollout plan: protected miss, demonstrated quality harm, negative
|
|
438
439
|
// economics. An inconclusive non-inferiority result (no evidence of harm) holds instead.
|
|
439
|
-
const rollback =
|
|
440
|
+
const rollback = findings.has('protected-retention-breach')
|
|
440
441
|
|| findings.has('quality-harm') || findings.has('negative-net-economics');
|
|
441
|
-
const decision = rollback ? 'ROLLBACK' :
|
|
442
|
+
const decision = ceilingDecision(upstreamDecision, rollback ? 'ROLLBACK' : findings.size > 0 ? 'HOLD' : 'PROMOTE');
|
|
442
443
|
const advisory = [...findings].some(item => item.startsWith('insufficient-') || item.startsWith('quality-metric-missing:')
|
|
443
444
|
|| item.startsWith('missing-input:') || item.startsWith('quality-outcomes-incomplete:')
|
|
444
445
|
|| item.startsWith('quality-non-inferiority-inconclusive:')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDecisionCeiling, ceilingDecision, GATE_RANK, GateDecisionUpgradeError } from '../../gates/lattice.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
@@ -10,8 +11,8 @@ import { admitEntry } from '../entry.js';
|
|
|
10
11
|
export class EnsembleContractError extends Error {
|
|
11
12
|
layer;
|
|
12
13
|
details;
|
|
13
|
-
constructor(message, layer = 'semantic', details = []) {
|
|
14
|
-
super(message);
|
|
14
|
+
constructor(message, layer = 'semantic', details = [], cause) {
|
|
15
|
+
super(message, { cause });
|
|
15
16
|
this.layer = layer;
|
|
16
17
|
this.details = details;
|
|
17
18
|
this.name = 'EnsembleContractError';
|
|
@@ -41,7 +42,6 @@ export const ENSEMBLE_DISAGREEMENT_PRIMITIVES = {
|
|
|
41
42
|
const UNCERTAINTY_COMBINING = new Set(['mean-probability-v1', 'jensen-shannon-v1']);
|
|
42
43
|
export const REQUIRED_PAIRED_METRICS = ['quality', 'calibration', 'risk-coverage', 'abstention', 'latency', 'tokens', 'cost', 'slice'];
|
|
43
44
|
const CEILING_FIELDS = ['members', 'attempts', 'deadlineMs', 'tokens', 'costMicros', 'concurrency', 'fallbackDepth'];
|
|
44
|
-
const GATE_RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
|
|
45
45
|
let validators = null;
|
|
46
46
|
function schemaValidator(kind) {
|
|
47
47
|
if (!validators) {
|
|
@@ -334,7 +334,7 @@ export function pairedDeltaFinding(threshold, item) {
|
|
|
334
334
|
export function ensembleIntegrityDecision(integrity, findingCount) {
|
|
335
335
|
const upstream = integrity.release_gate.decision;
|
|
336
336
|
const compromised = integrity.integrity_state === 'compromised' || integrity.compromise_labels.length > 0;
|
|
337
|
-
return upstream
|
|
337
|
+
return ceilingDecision(upstream, compromised ? 'ROLLBACK' : 'PROMOTE', findingCount > 0 ? 'HOLD' : 'PROMOTE');
|
|
338
338
|
}
|
|
339
339
|
/** A D17 report may keep or tighten the upstream #2037/#2048 decision, never loosen it. Every
|
|
340
340
|
* finding, passed flag and the decision are recomputed from the carried integrity fields and the
|
|
@@ -346,6 +346,15 @@ export function validateEnsembleIntegrityReport(value) {
|
|
|
346
346
|
const problems = [];
|
|
347
347
|
if (report.upstreamDecision !== report.integrity.release_gate.decision)
|
|
348
348
|
problems.push('upstream decision must be the eval-integrity release gate decision');
|
|
349
|
+
let upgrade;
|
|
350
|
+
try {
|
|
351
|
+
assertDecisionCeiling(report.decision, report.upstreamDecision, ensembleIntegrityDecision(report.integrity, report.findings.length));
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (!(error instanceof GateDecisionUpgradeError))
|
|
355
|
+
throw error;
|
|
356
|
+
upgrade = error;
|
|
357
|
+
}
|
|
349
358
|
if (GATE_RANK[report.decision] < GATE_RANK[report.upstreamDecision])
|
|
350
359
|
problems.push(`D17 cannot upgrade ${report.upstreamDecision} to ${report.decision}`);
|
|
351
360
|
if (report.decision === 'PROMOTE' && (report.findings.length > 0 || report.pairedDeltas.some(item => !item.passed)))
|
|
@@ -372,6 +381,8 @@ export function validateEnsembleIntegrityReport(value) {
|
|
|
372
381
|
problems.push(`decision ${report.decision} does not follow from its integrity and findings (${decision})`);
|
|
373
382
|
if (digest !== ensembleContractDigest(payload))
|
|
374
383
|
problems.push('integrity report digest does not match its content');
|
|
384
|
+
if (upgrade)
|
|
385
|
+
throw new EnsembleContractError('ensemble integrity report rejected', 'semantic', problems, upgrade);
|
|
375
386
|
semantic(problems, 'ensemble integrity report rejected');
|
|
376
387
|
return report;
|
|
377
388
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDecisionCeiling, ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, resolve } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -32,6 +33,10 @@ export function validateD17Artifact(name, value) {
|
|
|
32
33
|
admitEntry(value, name === 'report' || name === 'reportV2' ? reportLimits : limits);
|
|
33
34
|
if (!validators[name]?.(value))
|
|
34
35
|
throw new Error(`D17 invalid ${name} artifact`);
|
|
36
|
+
if (name === 'report' || name === 'reportV2') {
|
|
37
|
+
const report = value;
|
|
38
|
+
assertDecisionCeiling(report.decision, ceilingDecision('HOLD', report.integrity.release_gate.decision, report.integrity.compromise_labels.length ? 'ROLLBACK' : 'PROMOTE'));
|
|
39
|
+
}
|
|
35
40
|
if (name === 'splits') {
|
|
36
41
|
const manifest = value;
|
|
37
42
|
const ids = new Set(), inputs = new Set(), families = new Map();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { heldoutDigest, validateHeldoutAttempt, validateHeldoutInputs } from '../heldout/contract.js';
|
|
2
3
|
import { artifactPin } from '../validate.js';
|
|
3
4
|
import { aggregateEnsembleResults } from '../ensemble/aggregate.js';
|
|
@@ -145,7 +146,7 @@ export async function scoreD17Study(input, prepared) {
|
|
|
145
146
|
calibrationDiagnostics: { champion: binary('champion'), challenger: binary('challenger'), calibrated: false },
|
|
146
147
|
pending: ['D09 compatible member and aggregate calibration', 'blind gold audit and delayed repeats', 'separate extra-cost tradeoff approval',
|
|
147
148
|
'protected native report approval and eligibility'],
|
|
148
|
-
decision: input.integrity.release_gate.decision
|
|
149
|
+
decision: ceilingDecision('HOLD', input.integrity.release_gate.decision, input.integrity.compromise_labels.length ? 'ROLLBACK' : 'PROMOTE') };
|
|
149
150
|
validateD17Artifact('report', report);
|
|
150
151
|
return report;
|
|
151
152
|
}
|
|
@@ -325,7 +326,7 @@ export async function scoreD17StagedStudy(input, prepared, context) {
|
|
|
325
326
|
mappingDigest: heldoutDigest(verified[role].mapping), compatibility: { action: 'allow',
|
|
326
327
|
state: verified[role].compatibility.state, pinId: verified[role].compatibility.pinId } });
|
|
327
328
|
const approved = input.approvedCalibration;
|
|
328
|
-
const decision = input.integrity.release_gate.decision
|
|
329
|
+
const decision = ceilingDecision('HOLD', input.integrity.release_gate.decision, input.integrity.compromise_labels.length ? 'ROLLBACK' : 'PROMOTE');
|
|
329
330
|
const report = { schemaVersion: 'decision-d17-study-report/v2', scope: 'synthetic-staged-calibrated',
|
|
330
331
|
calibrated: true, d09Qualified: true, calibratedGate: true,
|
|
331
332
|
corpusDigest: heldoutDigest(prepared.corpus), preregistrationDigest: heldoutDigest(prepared.preregistration),
|
|
@@ -453,7 +454,7 @@ export function d17NativeIntegrity(input) {
|
|
|
453
454
|
// The ensemble integrity contract carries only the gate decision and reasons (eval-tool thresholds are dropped).
|
|
454
455
|
const gate = { decision: input.base.release_gate.decision, reasons: [...input.base.release_gate.reasons] };
|
|
455
456
|
const release_gate = gate.decision === 'PROMOTE' && missing.length
|
|
456
|
-
? { decision: 'HOLD', reasons: [...gate.reasons, ...missing.map(reason => `promotion withheld: ${reason}`)] } : gate;
|
|
457
|
+
? { decision: ceilingDecision(gate.decision, 'HOLD'), reasons: [...gate.reasons, ...missing.map(reason => `promotion withheld: ${reason}`)] } : gate;
|
|
457
458
|
const { sample_n, uncertainty, integrity_mode, fresh_workspace_required, fresh_workspace_verified, integrity_state, trusted_score_source, compromise_labels, weak_signal_reason } = input.base;
|
|
458
459
|
return { sample_n, uncertainty, paired_baseline: { ...input.pairedBaseline, d17TradeoffApprovalDigest: tradeoffDigest },
|
|
459
460
|
integrity_mode, fresh_workspace_required, fresh_workspace_verified, integrity_state, trusted_score_source,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDecisionCeiling } from '../gates/lattice.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
@@ -942,9 +943,7 @@ function normalizeEvalIntegrity(value) {
|
|
|
942
943
|
if (value.upstreamDisposition !== value.qualificationRelease.decision) {
|
|
943
944
|
throw new DecisionFeatureExportError('Feature export integrity must preserve the upstream qualification release decision');
|
|
944
945
|
}
|
|
945
|
-
|
|
946
|
-
throw new DecisionFeatureExportError(`Feature export integrity cannot upgrade ${value.upstreamDisposition} to ${value.disposition}`);
|
|
947
|
-
}
|
|
946
|
+
assertDecisionCeiling(value.disposition, value.upstreamDisposition);
|
|
948
947
|
const { digest, ...payload } = value;
|
|
949
948
|
if (digest !== digestEvalIntegrity(payload))
|
|
950
949
|
throw new DecisionFeatureExportError('Feature export integrity digest does not match its content');
|
|
@@ -966,9 +965,7 @@ function validateQualificationRelease(record) {
|
|
|
966
965
|
throw new DecisionFeatureExportError('Feature export qualification release digest does not match its content');
|
|
967
966
|
}
|
|
968
967
|
validateQualificationIntegrity(record.integrity);
|
|
969
|
-
|
|
970
|
-
throw new DecisionFeatureExportError('Feature export qualification release cannot upgrade its integrity release gate');
|
|
971
|
-
}
|
|
968
|
+
assertDecisionCeiling(record.decision, record.integrity.release_gate.decision, record.integrity.integrity_state === 'compromised' || record.integrity.compromise_labels.length ? 'ROLLBACK' : 'PROMOTE', ...record.gates.map(gate => gate.status === 'pass' ? 'PROMOTE' : 'HOLD'));
|
|
972
969
|
}
|
|
973
970
|
function validateQualificationIntegrity(value) {
|
|
974
971
|
if (!value || !Number.isSafeInteger(value.sample_n) || value.sample_n < 0
|
|
@@ -986,9 +983,6 @@ function validateQualificationIntegrity(value) {
|
|
|
986
983
|
function digestEvalIntegrity(value) {
|
|
987
984
|
return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
|
|
988
985
|
}
|
|
989
|
-
function integrityRank(decision) {
|
|
990
|
-
return { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 }[decision];
|
|
991
|
-
}
|
|
992
986
|
function activeExportHold(hold, subject, nowEpochMs) {
|
|
993
987
|
return hold.subject === subject && hold.expiresAt > nowEpochMs && hold.scope.includes('export');
|
|
994
988
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
@@ -425,7 +426,6 @@ export async function scoreHeldoutStudy(input) {
|
|
|
425
426
|
return { schemaVersion: 'decision-heldout-score/v1', source: frozen.priorRuns.some(run => run.source === 'injected-transport') ? 'injected-transport' : frozen.source, evidenceDigest: input.trustedEvidenceDigest, integrityDigest: input.trustedIntegrityDigest,
|
|
426
427
|
complete, diagnostics, approvedCalibration, calibrated: studyValidated ? true : calibrated, d09Qualified: studyValidated,
|
|
427
428
|
calibratedGate: studyValidated && study?.calibratedGate === true,
|
|
428
|
-
calibrationArtifactValidation: studyValidated ? 'performed-by-study-scorer' : 'not-performed', integrityProblems: problems, decision: input.integrity.release_gate.decision
|
|
429
|
-
|| input.integrity.compromise_labels.length > 0 ? 'ROLLBACK' : 'HOLD' };
|
|
429
|
+
calibrationArtifactValidation: studyValidated ? 'performed-by-study-scorer' : 'not-performed', integrityProblems: problems, decision: ceilingDecision('HOLD', input.integrity.release_gate.decision, input.integrity.compromise_labels.length > 0 ? 'ROLLBACK' : 'PROMOTE') };
|
|
430
430
|
}
|
|
431
431
|
//# sourceMappingURL=collector.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDecisionCeiling, ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
@@ -26,7 +27,6 @@ const SCHEMA_FILES = {
|
|
|
26
27
|
manifest: 'DecisionIssueTriageEvaluationManifest.v1.schema.json',
|
|
27
28
|
report: 'DecisionIssueTriageEvaluationReport.v1.schema.json',
|
|
28
29
|
};
|
|
29
|
-
const GATE_RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
|
|
30
30
|
const FINAL_FIELDS = ['finalLabels', 'finalDuplicateOf', 'resolution', 'closedAt'];
|
|
31
31
|
const RESPONSE_FIELDS = ['issueId', 'issueType', 'area', 'urgency', 'completeness', 'clarificationNeed', 'duplicate',
|
|
32
32
|
'uncertaintyProfile', 'accepted', 'actualModel', 'requestedModel', 'latencyMs', 'usage', 'usageReceipts', 'calls', 'retries', 'fallbacks',
|
|
@@ -156,9 +156,7 @@ function checkIssueTriageEvaluationReportShape(value) {
|
|
|
156
156
|
checkSchema('report', value);
|
|
157
157
|
const report = value;
|
|
158
158
|
const problems = [];
|
|
159
|
-
|
|
160
|
-
problems.push(`issue triage report cannot upgrade ${report.integrity.upstreamDecision} to ${report.decision}`);
|
|
161
|
-
}
|
|
159
|
+
assertDecisionCeiling(report.decision, report.integrity.upstreamDecision, gateDecision(report.integrity.upstreamDecision, report.integrity.findings));
|
|
162
160
|
if (report.decision === 'PROMOTE' && report.integrity.findings.length > 0)
|
|
163
161
|
problems.push('PROMOTE requires no findings');
|
|
164
162
|
if (report.decision === 'PROMOTE' && report.baselineComparison.qualityNonInferiority?.decision !== 'non-inferior') {
|
|
@@ -603,11 +601,7 @@ function correct(sample, which) {
|
|
|
603
601
|
&& observed.duplicate.issueId === sample.label.duplicateOf;
|
|
604
602
|
}
|
|
605
603
|
function gateDecision(upstream, findings) {
|
|
606
|
-
|
|
607
|
-
return 'ROLLBACK';
|
|
608
|
-
if (upstream === 'HOLD' || findings.length > 0)
|
|
609
|
-
return 'HOLD';
|
|
610
|
-
return 'PROMOTE';
|
|
604
|
+
return ceilingDecision(upstream, findings.length > 0 ? 'HOLD' : 'PROMOTE');
|
|
611
605
|
}
|
|
612
606
|
/** Supported interval level in bps, or null. The paired helpers accept integer levels strictly in (5000, 9999). */
|
|
613
607
|
function levelInBps(confidenceInterval) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { expectedQualificationCaseIds, validateCaseInventory } from './manifest.js';
|
|
2
3
|
import { deriveGateEvidence, DERIVED_GATE_EVIDENCE } from './gate-evidence.js';
|
|
3
4
|
// Negative evidence always overrides a positive suite result. A release reviewer
|
|
@@ -76,7 +77,7 @@ export function evaluateQualification(manifest, definitions = DECISION_RELEASE_G
|
|
|
76
77
|
schemaVersion: 'decision-qualification-report/v1',
|
|
77
78
|
runId: manifest.runId,
|
|
78
79
|
generatedAt: manifest.generatedAt,
|
|
79
|
-
decision: mandatoryFailed ? 'HOLD' : 'PROMOTE',
|
|
80
|
+
decision: ceilingDecision('PROMOTE', mandatoryFailed ? 'HOLD' : 'PROMOTE'),
|
|
80
81
|
gates,
|
|
81
82
|
};
|
|
82
83
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { canonicalSha256, matchEvidenceDigest } from '../../gates/stats/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* #2037/#2048 integrity modes whose verified state has a trusted score source.
|
|
@@ -133,9 +134,7 @@ export function buildQualificationReleaseRecord(executed, input) {
|
|
|
133
134
|
&& suites.length === manifest.cases.length && suites.every(item => item.outcome === 'pass' && item.verified)
|
|
134
135
|
&& report.gates.length === 7 && report.gates.every(gate => gate.status === 'pass')
|
|
135
136
|
&& report.decision === 'PROMOTE';
|
|
136
|
-
const decision = releaseDecision === 'ROLLBACK'
|
|
137
|
-
|| report.decision === 'ROLLBACK' || input.integrity.integrity_state === 'compromised' || input.integrity.compromise_labels.length > 0 ? 'ROLLBACK'
|
|
138
|
-
: complete && releaseDecision === 'PROMOTE' ? 'PROMOTE' : 'HOLD';
|
|
137
|
+
const decision = ceilingDecision(releaseDecision, report.decision, complete ? 'PROMOTE' : 'HOLD', input.integrity.integrity_state === 'compromised' || input.integrity.compromise_labels.length > 0 ? 'ROLLBACK' : 'PROMOTE');
|
|
139
138
|
const fields = {
|
|
140
139
|
schemaVersion: 'decision-qualification-release/v2',
|
|
141
140
|
runId: manifest.runId, sourceCommit: manifest.sourceCommit, dirty: manifest.dirty,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDecisionCeiling } from '../../gates/lattice.js';
|
|
1
2
|
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
2
3
|
import { ensembleIntegrityDecision, integrityMetadataFindings } from '../ensemble/contract.js';
|
|
3
4
|
import { pairedBinaryDifferenceInterval, pairedNonInferiority } from '../qualification/quality.js';
|
|
@@ -20,6 +21,7 @@ export function buildRoutingShadowReport(input) {
|
|
|
20
21
|
export function validateRoutingShadowReport(value, trustedPreregistrationDigest) {
|
|
21
22
|
checkRoutingSchema('shadowReport', value);
|
|
22
23
|
const report = value;
|
|
24
|
+
assertDecisionCeiling(report.decision, ensembleIntegrityDecision(report.integrity, report.reasons.length));
|
|
23
25
|
const rebuilt = computeRoutingShadowReport({
|
|
24
26
|
id: report.id, preregistration: report.preregistration, trustedPreregistrationDigest,
|
|
25
27
|
holdoutAccessedAt: report.holdoutAccessedAt, evaluatedAt: report.evaluatedAt,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision } from '../gates/lattice.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { DECISION_API_VERSION, DECISION_API_VERSION_STRUCTURED, } from './types.js';
|
|
3
4
|
import { reviewDigest } from './review/validate.js';
|
|
@@ -1135,11 +1136,8 @@ export function buildSdlcScreeningReleaseReport(input) {
|
|
|
1135
1136
|
const preregistered = evaluateSdlcScreeningPreregistration(input.preregistration, input.trustedPreregistrationDigest, input.heldout, input.nowEpochMs);
|
|
1136
1137
|
const integrityProblems = qualificationIntegrityAllowlistProblems(input.integrity);
|
|
1137
1138
|
const readable = !integrityProblems.includes('integrity-invalid');
|
|
1138
|
-
const compromised = readable && (input.integrity.
|
|
1139
|
-
|
|
1140
|
-
const decision = compromised ? 'ROLLBACK'
|
|
1141
|
-
: readable && integrityProblems.length === 0 && preregistered.decision === 'pass'
|
|
1142
|
-
&& input.integrity.release_gate.decision === 'PROMOTE' ? 'PROMOTE' : 'HOLD';
|
|
1139
|
+
const compromised = readable && (input.integrity.integrity_state === 'compromised' || input.integrity.compromise_labels.length > 0);
|
|
1140
|
+
const decision = ceilingDecision(readable ? input.integrity.release_gate.decision : 'HOLD', compromised ? 'ROLLBACK' : 'PROMOTE', integrityProblems.length === 0 && preregistered.decision === 'pass' ? 'PROMOTE' : 'HOLD');
|
|
1143
1141
|
const unsigned = {
|
|
1144
1142
|
schemaVersion: input.preregistration.schemaVersion === SDLC_SCREENING_PREREGISTRATION_VERSION_V2
|
|
1145
1143
|
? SDLC_SCREENING_RELEASE_VERSION_V2 : SDLC_SCREENING_RELEASE_VERSION,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDecisionCeiling, ceilingDecision } from '../../gates/lattice.js';
|
|
1
2
|
import { pairedBinaryDifferenceInterval, wilsonScoreInterval } from '../qualification/quality.js';
|
|
2
3
|
import { qualificationIntegrityAllowlistProblems } from '../qualification/release.js';
|
|
3
4
|
import { checkSensitivitySchema, sensitivityDigest, validateSensitivityReport } from './contract.js';
|
|
@@ -128,9 +129,8 @@ export function buildComparativeReplayReport(input) {
|
|
|
128
129
|
throw new Error('operator review ingestion is not implemented');
|
|
129
130
|
findings.add('reviewer-audit-pending');
|
|
130
131
|
const upstreamDecision = input.integrity.release_gate.decision;
|
|
131
|
-
const decision = upstreamDecision
|
|
132
|
-
|| input.integrity.compromise_labels.length > 0 ? 'ROLLBACK'
|
|
133
|
-
: upstreamDecision === 'HOLD' || findings.size > 0 ? 'HOLD' : 'PROMOTE';
|
|
132
|
+
const decision = ceilingDecision(upstreamDecision, input.integrity.integrity_state === 'compromised'
|
|
133
|
+
|| input.integrity.compromise_labels.length > 0 ? 'ROLLBACK' : 'PROMOTE', findings.size > 0 ? 'HOLD' : 'PROMOTE');
|
|
134
134
|
const payload = {
|
|
135
135
|
schemaVersion: 'decision-comparative-replay-report/v1', mode: 'shadow',
|
|
136
136
|
semantics: 'associative-sensitivity-not-causal', actionAuthorization: 'not-authorized',
|
|
@@ -147,7 +147,9 @@ export function buildComparativeReplayReport(input) {
|
|
|
147
147
|
}
|
|
148
148
|
export function validateComparativeReplayReport(report, input) {
|
|
149
149
|
checkSensitivitySchema('comparativeReport', report);
|
|
150
|
+
assertDecisionCeiling(report.decision, report.upstreamDecision, input.integrity.release_gate.decision);
|
|
150
151
|
const expected = buildComparativeReplayReport(input);
|
|
152
|
+
assertDecisionCeiling(report.decision, expected.decision);
|
|
151
153
|
if (sensitivityDigest(report) !== sensitivityDigest(expected))
|
|
152
154
|
throw new Error('comparative replay report differs from bound inputs');
|
|
153
155
|
}
|
package/dist/src/gates/driver.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertGateReportDecision } from './report.js';
|
|
1
2
|
import { lstatSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -159,11 +160,12 @@ export async function validateGateFile(kind, filePath, options = {}) {
|
|
|
159
160
|
else {
|
|
160
161
|
// Full report validation needs the trusted evaluation context
|
|
161
162
|
// (`validateGateReport` re-derives the report byte-identically).
|
|
162
|
-
// `validate` checks the closed schema,
|
|
163
|
+
// `validate` checks component ceilings, the closed schema, kind and self-digest,
|
|
163
164
|
// so rewritten evidence with a stale digest still fails here.
|
|
164
165
|
const report = validateGateDocument(document);
|
|
165
166
|
if (!isRecord(document) || document.kind !== 'GateReport')
|
|
166
167
|
throw new Error('expected kind GateReport');
|
|
168
|
+
assertGateReportDecision(report);
|
|
167
169
|
const { digest, ...fields } = report;
|
|
168
170
|
if (typeof digest !== 'string' || artifactDigest(fields) !== digest) {
|
|
169
171
|
throw new Error('report digest does not match its content');
|
|
@@ -276,83 +278,10 @@ export function listGatePacks(options = {}) {
|
|
|
276
278
|
return summaries.filter(entry => entry.namespace === options.namespace);
|
|
277
279
|
return summaries;
|
|
278
280
|
}
|
|
279
|
-
/**
|
|
280
|
-
* Packs referenced by one rule's `enforcedBy` frontmatter (#2830 stub; full
|
|
281
|
-
* coverage is #2839). Scans bundled and project-local `rules/*.md` for an
|
|
282
|
-
* `enforcedBy: [aiwg:<bundle>/<pack>#<gate>]` list naming the rule. Returns
|
|
283
|
-
* the referenced pack ids (without gate fragments). Empty when no rule
|
|
284
|
-
* declares `enforcedBy` — rules remain guidance until they name a check.
|
|
285
|
-
*/
|
|
281
|
+
/** List the distinct packs named by validated rule coverage declarations. */
|
|
286
282
|
export async function packsReferencedByRule(ruleId, options = {}) {
|
|
287
|
-
const {
|
|
288
|
-
|
|
289
|
-
const root = rootDir(options);
|
|
290
|
-
for (const sub of ['frameworks', 'addons', 'extensions']) {
|
|
291
|
-
roots.push(path.join(root, 'agentic/code', sub));
|
|
292
|
-
}
|
|
293
|
-
roots.push(path.join(baseDir(options), '.aiwg'));
|
|
294
|
-
const ruleFiles = [];
|
|
295
|
-
const walk = async (dir, depth) => {
|
|
296
|
-
if (depth > 5 || ruleFiles.length > 500)
|
|
297
|
-
return;
|
|
298
|
-
let entries;
|
|
299
|
-
try {
|
|
300
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
306
|
-
if (ruleFiles.length > 500)
|
|
307
|
-
return;
|
|
308
|
-
const full = path.join(dir, entry.name);
|
|
309
|
-
if (entry.isDirectory()) {
|
|
310
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.git'))
|
|
311
|
-
continue;
|
|
312
|
-
await walk(full, depth + 1);
|
|
313
|
-
}
|
|
314
|
-
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
315
|
-
if (!full.replace(/\\/g, '/').includes('/rules/'))
|
|
316
|
-
continue;
|
|
317
|
-
ruleFiles.push(full);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
for (const dir of roots)
|
|
322
|
-
await walk(dir, 0);
|
|
323
|
-
const referenced = new Set();
|
|
324
|
-
for (const file of ruleFiles) {
|
|
325
|
-
let content;
|
|
326
|
-
try {
|
|
327
|
-
const st = await stat(file);
|
|
328
|
-
if (st.size > 65_536)
|
|
329
|
-
continue;
|
|
330
|
-
content = await readRuleFile(file, 'utf8');
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
336
|
-
if (!match)
|
|
337
|
-
continue;
|
|
338
|
-
const frontmatter = match[1];
|
|
339
|
-
if (!frontmatter.includes('enforcedBy'))
|
|
340
|
-
continue;
|
|
341
|
-
const ruleName = path.basename(file, '.md');
|
|
342
|
-
const dirName = path.basename(path.dirname(file));
|
|
343
|
-
const names = new Set([ruleName, dirName === 'rules' ? null : `${dirName}/${ruleName}`, file]);
|
|
344
|
-
if (!names.has(ruleId) && ![...names].filter(Boolean).some(name => ruleId.endsWith(String(name))))
|
|
345
|
-
continue;
|
|
346
|
-
for (const line of frontmatter.split('\n')) {
|
|
347
|
-
const trimmed = line.trim().replace(/^-\s*/, '');
|
|
348
|
-
const id = trimmed.match(/(aiwg|framework|addon|extension|project):[^\s,'"\]]+/)?.[1]
|
|
349
|
-
? trimmed.match(/((?:aiwg|framework|addon|extension|project):[^\s,'"\]]+)/)?.[1]
|
|
350
|
-
: null;
|
|
351
|
-
if (id)
|
|
352
|
-
referenced.add(id.split('#')[0]);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return [...referenced].sort();
|
|
283
|
+
const { listRuleGateCoverage } = await import('../rules/gate-coverage.js');
|
|
284
|
+
return [...new Set(listRuleGateCoverage(options, ruleId).flatMap(rule => rule.enforcedBy.map(reference => reference.split('#')[0])))].sort();
|
|
356
285
|
}
|
|
357
286
|
/** Show one pack by full id, rest-path or short pack name. */
|
|
358
287
|
export function showGatePack(id, options = {}) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ceilingDecision, maxOutcome } from './lattice.js';
|
|
1
2
|
import { qualificationIntegrityAllowlistProblems } from '../decision/qualification/release.js';
|
|
2
3
|
import { artifactDigest } from '../decision/validate.js';
|
|
3
4
|
import { evaluatePredicate } from '../decision/predicates.js';
|
|
@@ -12,11 +13,7 @@ import { qualifyGateParameter } from './types.js';
|
|
|
12
13
|
export class GateEvaluationError extends Error {
|
|
13
14
|
constructor(message) { super(message); this.name = 'GateEvaluationError'; }
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
/** Shared outcome lattice. The maximum never upgrades any component. */
|
|
17
|
-
export function maxOutcome(...outcomes) {
|
|
18
|
-
return outcomes.reduce((worst, outcome) => (RANK[outcome] > RANK[worst] ? outcome : worst), 'PROMOTE');
|
|
19
|
-
}
|
|
16
|
+
export { maxOutcome } from './lattice.js';
|
|
20
17
|
/**
|
|
21
18
|
* Per-gate outcome from its own status: `pass` promotes, `fail` takes the
|
|
22
19
|
* gate's `onFail`, `insufficient` takes `onInsufficient` (always HOLD by load
|
|
@@ -447,11 +444,7 @@ function upstreamOutcome(upstream) {
|
|
|
447
444
|
const compromised = problems.includes('compromised')
|
|
448
445
|
|| release === 'ROLLBACK'
|
|
449
446
|
|| upstream.metadata.integrity_state === 'compromised';
|
|
450
|
-
|
|
451
|
-
return { ceiling: 'ROLLBACK', problems };
|
|
452
|
-
if (problems.length || release === 'HOLD')
|
|
453
|
-
return { ceiling: 'HOLD', problems };
|
|
454
|
-
return { ceiling: 'PROMOTE', problems };
|
|
447
|
+
return { ceiling: ceilingDecision(release ?? 'HOLD', compromised ? 'ROLLBACK' : 'PROMOTE', problems.length ? 'HOLD' : 'PROMOTE'), problems };
|
|
455
448
|
}
|
|
456
449
|
/**
|
|
457
450
|
* Pure, deterministic gate evaluation. The binding is resolved internally
|
|
@@ -685,7 +678,7 @@ export function evaluateGates(input) {
|
|
|
685
678
|
const packOutcome = maxOutcome(...gateEvidence.map(entry => entry.outcome));
|
|
686
679
|
const { ceiling: upstreamCeiling, problems: upstreamProblems } = upstreamOutcome(upstream);
|
|
687
680
|
const bindingCeiling = binding.spec.ceiling ?? 'PROMOTE';
|
|
688
|
-
const decision =
|
|
681
|
+
const decision = ceilingDecision(packOutcome, upstreamCeiling, bindingCeiling);
|
|
689
682
|
const body = {
|
|
690
683
|
apiVersion: binding.apiVersion,
|
|
691
684
|
kind: 'GateReport',
|