@aiwg/cli 2026.9.20 → 2026.9.24
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/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/cli.js +23 -18
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
- package/dist/src/artifacts/query-engine.js +14 -4
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +282 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared detector for secret material in portable decision artifacts
|
|
4
|
+
* (receipts, batch receipts, result-cache entries).
|
|
5
|
+
*
|
|
6
|
+
* Detection runs over the canonical JSON serialization so that keys and
|
|
7
|
+
* string values are both inspected, matching the projection policy guard.
|
|
8
|
+
* Rules are deliberately conservative to avoid rejecting legitimate data:
|
|
9
|
+
*
|
|
10
|
+
* - bearer values: `Bearer <token>` where the token is at least 16 token
|
|
11
|
+
* characters (plain prose such as "bearer of news" does not match);
|
|
12
|
+
* - PEM private keys: any `-----BEGIN ... PRIVATE KEY-----` armor header;
|
|
13
|
+
* - private locators: `vault://` and `secret://` URIs, and string values that
|
|
14
|
+
* begin with a Vault KV-v2 data path (`secret/data/...` or `kv/data/...`);
|
|
15
|
+
* - secret-derived hashes: keys such as `secretHash`, `tokenSha256Hash`,
|
|
16
|
+
* `apiKeyHash`, `credentialDigestHash`;
|
|
17
|
+
* - embedded secret values: keys named exactly `secret`, `credential`,
|
|
18
|
+
* `token`, `apiKey`/`api_key`/`api-key`, or those names suffixed with
|
|
19
|
+
* `Value` or `Header`.
|
|
20
|
+
*
|
|
21
|
+
* Logical credential references (`credentialRef`, e.g. `typesafe.jev.x`) are
|
|
22
|
+
* intentionally allowed: bindings carry them and batch target identities embed
|
|
23
|
+
* them. A reference whose value is itself a vault/secret locator still fails
|
|
24
|
+
* through the locator rule.
|
|
25
|
+
*/
|
|
26
|
+
const RULES = [
|
|
27
|
+
/\bbearer\s+[a-z0-9._~+/-]{16,}=*/i,
|
|
28
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/,
|
|
29
|
+
/(?:vault|secret):\/\//i,
|
|
30
|
+
/"(?:secret|kv)\/data\/[^"\s]/i,
|
|
31
|
+
// Key rules also match keys inside JSON embedded in a string value (escaped quotes).
|
|
32
|
+
/\\?"(?:secret|credential|token|api[_-]?key)[^"\\]*hash\\?"\s*:/i,
|
|
33
|
+
/\\?"(?:secret|credential|token|api[_-]?key)(?:Value|Header)?\\?"\s*:/i,
|
|
34
|
+
];
|
|
35
|
+
function serialize(value) {
|
|
36
|
+
try {
|
|
37
|
+
return canonicalJson(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return JSON.stringify(value) ?? '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function containsPortableSecretMaterial(value) {
|
|
44
|
+
const serialized = serialize(value);
|
|
45
|
+
return RULES.some(rule => rule.test(serialized));
|
|
46
|
+
}
|
|
47
|
+
/** Throws `makeError(message)`; the message never echoes the offending material. */
|
|
48
|
+
export function assertNoPortableSecretMaterial(value, name, makeError) {
|
|
49
|
+
if (containsPortableSecretMaterial(value)) {
|
|
50
|
+
throw makeError(`${name} contains forbidden credential or private-locator material`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=portable-secrets.js.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
2
|
+
import { resolveJsonPointer } from './validate.js';
|
|
3
|
+
/** Ordered data classes; a field above the policy ceiling is denied egress. */
|
|
4
|
+
export const DECISION_SENSITIVITY_ORDER = ['public', 'internal', 'confidential', 'restricted'];
|
|
5
|
+
const DEFAULT_MAX_SENSITIVITY = 'confidential';
|
|
6
|
+
export class DecisionProjectionError extends Error {
|
|
7
|
+
reason;
|
|
8
|
+
constructor(reason, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
this.name = 'DecisionProjectionError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Deterministically creates the only state object an adapter may receive.
|
|
16
|
+
* Callers must run this before credential resolution or transport dispatch.
|
|
17
|
+
*/
|
|
18
|
+
export async function projectDecisionState(input, policy, options = {}) {
|
|
19
|
+
validateProjectionPolicy(policy);
|
|
20
|
+
const incompleteContext = options.incompleteContext === true;
|
|
21
|
+
if (incompleteContext && !policy.allowIncompleteContext) {
|
|
22
|
+
throw new DecisionProjectionError('data-boundary-denied', 'incomplete material context is not authorized by projection policy');
|
|
23
|
+
}
|
|
24
|
+
const state = {};
|
|
25
|
+
const included = [];
|
|
26
|
+
let subject;
|
|
27
|
+
for (const field of [...policy.fields].sort((a, b) => a.output.localeCompare(b.output))) {
|
|
28
|
+
const resolved = resolveJsonPointer(input, field.pointer);
|
|
29
|
+
if (!resolved.found)
|
|
30
|
+
throw new DecisionProjectionError('invalid-input', 'required projection field is missing');
|
|
31
|
+
state[field.output] = structuredClone(resolved.value);
|
|
32
|
+
subject ??= field.subject;
|
|
33
|
+
included.push({
|
|
34
|
+
pointer: field.pointer, output: field.output, source: field.source, trust: field.trust,
|
|
35
|
+
sensitivity: field.sensitivity, retentionClass: field.retentionClass,
|
|
36
|
+
accessScopes: [...field.accessScopes], exportPolicy: field.exportPolicy,
|
|
37
|
+
deletionPolicy: field.deletionPolicy, backupPolicy: field.backupPolicy,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const canonical = canonicalJson(state);
|
|
41
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical));
|
|
42
|
+
return {
|
|
43
|
+
state,
|
|
44
|
+
evidence: {
|
|
45
|
+
policyVersion: policy.version,
|
|
46
|
+
subject: subject,
|
|
47
|
+
included,
|
|
48
|
+
incompleteContext,
|
|
49
|
+
automaticActionAllowed: !incompleteContext,
|
|
50
|
+
projectedDigest: `sha256:${Buffer.from(digest).toString('hex')}`,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Enforces the projection boundary before any credential lookup or transport.
|
|
56
|
+
* The credential resolver receives no state, and dispatch receives only the
|
|
57
|
+
* minimized projection rather than the ambient input object.
|
|
58
|
+
*/
|
|
59
|
+
export async function dispatchProjectedDecisionState(input, policy, handlers, options = {}) {
|
|
60
|
+
const projected = await projectDecisionState(input, policy, options);
|
|
61
|
+
const credential = await handlers.resolveCredential();
|
|
62
|
+
return handlers.dispatch({
|
|
63
|
+
state: Object.freeze(projected.state),
|
|
64
|
+
evidence: Object.freeze(projected.evidence),
|
|
65
|
+
credential,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function validateProjectionPolicy(policy) {
|
|
69
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
70
|
+
throw new DecisionProjectionError('invalid-policy', 'projection policy must be an object');
|
|
71
|
+
}
|
|
72
|
+
rejectUnknownKeys(policy, ['version', 'provider', 'model', 'origin', 'region', 'purpose', 'allowIncompleteContext', 'maxSensitivity', 'fields'], 'projection policy');
|
|
73
|
+
if (![policy.version, policy.provider, policy.model, policy.origin, policy.region, policy.purpose]
|
|
74
|
+
.every(value => typeof value === 'string' && value.trim().length > 0)
|
|
75
|
+
|| typeof policy.allowIncompleteContext !== 'boolean' || !Array.isArray(policy.fields)) {
|
|
76
|
+
throw new DecisionProjectionError('invalid-policy', 'projection policy identity and fields are invalid');
|
|
77
|
+
}
|
|
78
|
+
// Scan the whole portable control object, not just individual field entries.
|
|
79
|
+
// Destination and identity strings are portable too and must never carry secrets.
|
|
80
|
+
rejectPortableSecretMaterial(policy, 'projection policy');
|
|
81
|
+
let normalizedOrigin;
|
|
82
|
+
try {
|
|
83
|
+
normalizedOrigin = new URL(policy.origin);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new DecisionProjectionError('invalid-policy', 'projection origin must be an absolute HTTPS URL');
|
|
87
|
+
}
|
|
88
|
+
if (normalizedOrigin.protocol !== 'https:' || normalizedOrigin.username || normalizedOrigin.password) {
|
|
89
|
+
throw new DecisionProjectionError('invalid-policy', 'projection origin must be credential-free HTTPS');
|
|
90
|
+
}
|
|
91
|
+
if (policy.maxSensitivity !== undefined && !DECISION_SENSITIVITY_ORDER.includes(policy.maxSensitivity)) {
|
|
92
|
+
throw new DecisionProjectionError('invalid-policy', 'projection sensitivity ceiling is invalid');
|
|
93
|
+
}
|
|
94
|
+
// Region is a host-declared deployment attribute; an unknown region is never authorized.
|
|
95
|
+
if (policy.region.trim().toLowerCase() === 'unknown') {
|
|
96
|
+
throw new DecisionProjectionError('data-boundary-denied', 'projection region is unknown');
|
|
97
|
+
}
|
|
98
|
+
const ceiling = DECISION_SENSITIVITY_ORDER.indexOf(policy.maxSensitivity ?? DEFAULT_MAX_SENSITIVITY);
|
|
99
|
+
if (!policy.fields.length)
|
|
100
|
+
throw new DecisionProjectionError('invalid-policy', 'projection policy must allow at least one field');
|
|
101
|
+
const outputs = new Set();
|
|
102
|
+
let subject;
|
|
103
|
+
for (const field of policy.fields) {
|
|
104
|
+
if (!field || typeof field !== 'object' || Array.isArray(field)) {
|
|
105
|
+
throw new DecisionProjectionError('invalid-policy', 'projection field must be an object');
|
|
106
|
+
}
|
|
107
|
+
rejectUnknownKeys(field, [
|
|
108
|
+
'pointer', 'output', 'source', 'subject', 'trust', 'sensitivity', 'purpose', 'retentionClass',
|
|
109
|
+
'accessScopes', 'exportPolicy', 'deletionPolicy', 'backupPolicy', 'allowedProviders',
|
|
110
|
+
'allowedModels', 'allowedOrigins', 'allowedRegions',
|
|
111
|
+
], 'projection field');
|
|
112
|
+
if (typeof field.pointer !== 'string' || !field.pointer.startsWith('/')
|
|
113
|
+
|| typeof field.output !== 'string' || !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(field.output)) {
|
|
114
|
+
throw new DecisionProjectionError('invalid-policy', 'projection fields require JSON pointers and portable output names');
|
|
115
|
+
}
|
|
116
|
+
if (outputs.has(field.output))
|
|
117
|
+
throw new DecisionProjectionError('invalid-policy', 'duplicate projection output');
|
|
118
|
+
outputs.add(field.output);
|
|
119
|
+
subject ??= field.subject;
|
|
120
|
+
if (!field.subject || field.subject !== subject) {
|
|
121
|
+
throw new DecisionProjectionError('invalid-policy', 'one projection may contain exactly one stable subject');
|
|
122
|
+
}
|
|
123
|
+
if (![field.source, field.subject, field.purpose, field.retentionClass]
|
|
124
|
+
.every(value => typeof value === 'string' && value.trim().length > 0)
|
|
125
|
+
|| ![field.accessScopes, field.allowedProviders, field.allowedModels, field.allowedOrigins, field.allowedRegions]
|
|
126
|
+
.every(value => Array.isArray(value) && value.length > 0
|
|
127
|
+
&& value.every(item => typeof item === 'string' && item.trim().length > 0))) {
|
|
128
|
+
throw new DecisionProjectionError('invalid-policy', 'projection field lacks provenance or lifecycle metadata');
|
|
129
|
+
}
|
|
130
|
+
const normalizedAllowedOrigins = field.allowedOrigins.map(origin => normalizeAuthorizedOrigin(origin));
|
|
131
|
+
if (field.purpose !== policy.purpose || !field.allowedProviders?.includes(policy.provider)
|
|
132
|
+
|| !field.allowedModels?.includes(policy.model) || !normalizedAllowedOrigins?.includes(normalizedOrigin.origin)
|
|
133
|
+
|| !field.allowedRegions?.includes(policy.region)) {
|
|
134
|
+
throw new DecisionProjectionError('data-boundary-denied', 'projection field is not authorized for the selected destination');
|
|
135
|
+
}
|
|
136
|
+
if (!field.source || !field.retentionClass || !field.allowedProviders.length || !field.allowedModels.length
|
|
137
|
+
|| !field.allowedOrigins.length || !field.allowedRegions.length || !field.accessScopes?.length
|
|
138
|
+
|| !['verified', 'untrusted'].includes(field.trust)
|
|
139
|
+
|| !['public', 'internal', 'confidential', 'restricted'].includes(field.sensitivity)
|
|
140
|
+
|| !['denied', 'sanitized'].includes(field.exportPolicy)
|
|
141
|
+
|| !['erase', 'tombstone'].includes(field.deletionPolicy)
|
|
142
|
+
|| !['expire-with-primary', 'not-persisted'].includes(field.backupPolicy)) {
|
|
143
|
+
throw new DecisionProjectionError('invalid-policy', 'projection field lacks provenance or lifecycle metadata');
|
|
144
|
+
}
|
|
145
|
+
// Data-class rules: a field above the destination ceiling never egresses, and
|
|
146
|
+
// restricted material may not be exported or outlive its primary record.
|
|
147
|
+
if (DECISION_SENSITIVITY_ORDER.indexOf(field.sensitivity) > ceiling) {
|
|
148
|
+
throw new DecisionProjectionError('data-boundary-denied', 'projection field exceeds the destination sensitivity ceiling');
|
|
149
|
+
}
|
|
150
|
+
if (field.sensitivity === 'restricted' && (field.exportPolicy !== 'denied' || field.backupPolicy !== 'not-persisted')) {
|
|
151
|
+
throw new DecisionProjectionError('invalid-policy', 'restricted projection fields must deny export and persistence');
|
|
152
|
+
}
|
|
153
|
+
rejectPortableSecretMaterial(field, 'projection field');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Partitions projected state by host-declared trust so adapters can keep
|
|
158
|
+
* verified evidence structurally separate from untrusted content. Delimiters
|
|
159
|
+
* are not relied on: the partition is a distinct object key.
|
|
160
|
+
*/
|
|
161
|
+
export function partitionProjectedState(state, evidence) {
|
|
162
|
+
const verified = {};
|
|
163
|
+
const untrusted = {};
|
|
164
|
+
const values = state && typeof state === 'object' && !Array.isArray(state) ? state : {};
|
|
165
|
+
for (const field of evidence.included) {
|
|
166
|
+
if (!Object.hasOwn(values, field.output))
|
|
167
|
+
continue;
|
|
168
|
+
(field.trust === 'verified' ? verified : untrusted)[field.output] = values[field.output];
|
|
169
|
+
}
|
|
170
|
+
return { verified, untrusted };
|
|
171
|
+
}
|
|
172
|
+
/** Normalizes a projection or adapter origin for exact endpoint binding. */
|
|
173
|
+
export function normalizeProjectionOrigin(origin) {
|
|
174
|
+
try {
|
|
175
|
+
return new URL(origin).origin;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new DecisionProjectionError('invalid-policy', 'projection origin must be an absolute HTTPS URL');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function normalizeAuthorizedOrigin(origin) {
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = new URL(origin);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new DecisionProjectionError('invalid-policy', 'allowed origins must be absolute HTTPS origins');
|
|
188
|
+
}
|
|
189
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.pathname !== '/'
|
|
190
|
+
|| parsed.search || parsed.hash) {
|
|
191
|
+
throw new DecisionProjectionError('invalid-policy', 'allowed origins must be credential-free HTTPS origins');
|
|
192
|
+
}
|
|
193
|
+
return parsed.origin;
|
|
194
|
+
}
|
|
195
|
+
function rejectUnknownKeys(value, allowed, name) {
|
|
196
|
+
const unknown = Object.keys(value).filter(key => !allowed.includes(key));
|
|
197
|
+
if (unknown.length)
|
|
198
|
+
throw new DecisionProjectionError('invalid-policy', `${name} contains unsupported control fields`);
|
|
199
|
+
}
|
|
200
|
+
function rejectPortableSecretMaterial(value, name) {
|
|
201
|
+
const serialized = canonicalJson(value);
|
|
202
|
+
if (/bearer\s+[a-z0-9._~+/=-]+/i.test(serialized)
|
|
203
|
+
|| /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(serialized)
|
|
204
|
+
|| /(?:vault|secret):\/\//i.test(serialized)
|
|
205
|
+
|| /"(?:secret|credential|token|api[_-]?key)[^"]*hash"\s*:/i.test(serialized)
|
|
206
|
+
|| /"(?:secret|credential|token|api[_-]?key)(?:Ref|Value|Header)?"\s*:/i.test(serialized)) {
|
|
207
|
+
throw new DecisionProjectionError('invalid-policy', `${name} contains forbidden credential or private-locator material`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { format } from 'node:util';
|
|
2
|
+
const CONSOLE_METHODS = ['log', 'info', 'debug', 'warn', 'error', 'trace'];
|
|
3
|
+
const STDERR_METHODS = new Set(['warn', 'error', 'trace']);
|
|
4
|
+
function errorText(error) {
|
|
5
|
+
if (error instanceof Error) {
|
|
6
|
+
const cause = error.cause === undefined ? '' : `\ncause: ${errorText(error.cause)}`;
|
|
7
|
+
return `${error.name}: ${error.message}\n${error.stack ?? ''}${cause}`;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(error) ?? String(error);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return String(error);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Captures what the process emits while `operation` runs: stream writes to
|
|
18
|
+
* stdout/stderr, console output (which test runners may route around the
|
|
19
|
+
* streams), and the text of a thrown error. Output still reaches the original
|
|
20
|
+
* sinks. Captures are process-wide, so concurrent work in the same process is
|
|
21
|
+
* captured too; run the qualification alone. Writes from child processes are
|
|
22
|
+
* not captured and must be collected by the caller.
|
|
23
|
+
*/
|
|
24
|
+
export async function captureQualificationLifetime(operation) {
|
|
25
|
+
const stdout = [];
|
|
26
|
+
const stderr = [];
|
|
27
|
+
const originalStdout = process.stdout.write;
|
|
28
|
+
const originalStderr = process.stderr.write;
|
|
29
|
+
const originalConsole = Object.fromEntries(CONSOLE_METHODS.map(name => [name, console[name]]));
|
|
30
|
+
const tap = (sink, original) => function (chunk, ...rest) {
|
|
31
|
+
sink.push(typeof chunk === 'string' ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk));
|
|
32
|
+
return original.call(this, chunk, ...rest);
|
|
33
|
+
};
|
|
34
|
+
process.stdout.write = tap(stdout, originalStdout);
|
|
35
|
+
process.stderr.write = tap(stderr, originalStderr);
|
|
36
|
+
for (const name of CONSOLE_METHODS) {
|
|
37
|
+
console[name] = (...args) => {
|
|
38
|
+
(STDERR_METHODS.has(name) ? stderr : stdout).push(`${format(...args)}\n`);
|
|
39
|
+
originalConsole[name].apply(console, args);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
let result;
|
|
43
|
+
let thrown = '';
|
|
44
|
+
let threw = false;
|
|
45
|
+
try {
|
|
46
|
+
result = await operation();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
threw = true;
|
|
50
|
+
thrown = errorText(error);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
process.stdout.write = originalStdout;
|
|
54
|
+
process.stderr.write = originalStderr;
|
|
55
|
+
for (const name of CONSOLE_METHODS)
|
|
56
|
+
console[name] = originalConsole[name];
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...(threw ? {} : { result }), threw,
|
|
60
|
+
captures: [
|
|
61
|
+
{ surface: 'stdout', content: stdout.join('') },
|
|
62
|
+
{ surface: 'stderr', content: stderr.join('') },
|
|
63
|
+
{ surface: 'thrown-error', content: thrown },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { measurePairedMovement } from './quality.js';
|
|
2
|
+
// Smoothing keeps PSI finite for categories that appear in only one sample.
|
|
3
|
+
// It is a reporting convention, not a statistical test.
|
|
4
|
+
const PSI_EPSILON = 1e-4;
|
|
5
|
+
function validPlan(plan) {
|
|
6
|
+
return Number.isSafeInteger(plan.minimumN) && plan.minimumN >= 1
|
|
7
|
+
&& Number.isFinite(plan.maximumTotalVariation) && plan.maximumTotalVariation >= 0 && plan.maximumTotalVariation <= 1
|
|
8
|
+
&& Number.isFinite(plan.maximumPopulationStability) && plan.maximumPopulationStability >= 0;
|
|
9
|
+
}
|
|
10
|
+
function proportions(values, categories) {
|
|
11
|
+
const counts = new Map();
|
|
12
|
+
for (const value of values)
|
|
13
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
14
|
+
return Object.fromEntries(categories.map(category => [category, (counts.get(category) ?? 0) / values.length]));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compares two categorical samples: model outputs for output drift, or input
|
|
18
|
+
* slice membership for population drift. Too few observations are
|
|
19
|
+
* `insufficient-evidence`, never `stable`.
|
|
20
|
+
*/
|
|
21
|
+
export function measureCategoricalDrift(reference, candidate, plan) {
|
|
22
|
+
if (!validPlan(plan) || [...reference, ...candidate].some(value => typeof value !== 'string' || !value.trim())) {
|
|
23
|
+
throw new Error('categorical drift requires a valid plan and nonempty categorical values');
|
|
24
|
+
}
|
|
25
|
+
const categories = [...new Set([...reference, ...candidate])].sort();
|
|
26
|
+
const insufficient = reference.length < plan.minimumN || candidate.length < plan.minimumN;
|
|
27
|
+
const left = reference.length ? proportions(reference, categories) : {};
|
|
28
|
+
const right = candidate.length ? proportions(candidate, categories) : {};
|
|
29
|
+
let totalVariation = 0;
|
|
30
|
+
let populationStability = 0;
|
|
31
|
+
if (!insufficient) {
|
|
32
|
+
for (const category of categories) {
|
|
33
|
+
const p = left[category];
|
|
34
|
+
const q = right[category];
|
|
35
|
+
totalVariation += Math.abs(p - q) / 2;
|
|
36
|
+
const ps = Math.max(p, PSI_EPSILON);
|
|
37
|
+
const qs = Math.max(q, PSI_EPSILON);
|
|
38
|
+
populationStability += (qs - ps) * Math.log(qs / ps);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const reasons = [];
|
|
42
|
+
if (insufficient)
|
|
43
|
+
reasons.push('insufficient-samples');
|
|
44
|
+
if (!insufficient && totalVariation > plan.maximumTotalVariation)
|
|
45
|
+
reasons.push('total-variation');
|
|
46
|
+
if (!insufficient && populationStability > plan.maximumPopulationStability)
|
|
47
|
+
reasons.push('population-stability');
|
|
48
|
+
return {
|
|
49
|
+
schemaVersion: 'decision-categorical-drift/v1', referenceN: reference.length, candidateN: candidate.length,
|
|
50
|
+
categories, reference: left, candidate: right, totalVariation, populationStability,
|
|
51
|
+
decision: insufficient ? 'insufficient-evidence' : reasons.length ? 'drift' : 'stable', reasons,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Repeated-run label movement against a preregistered bound. Stable only when
|
|
56
|
+
* the whole 95% interval is within the bound; drift only when it is entirely
|
|
57
|
+
* above; otherwise the sample cannot decide.
|
|
58
|
+
*/
|
|
59
|
+
export function measureLabelStability(pairs, maximumChangedRate) {
|
|
60
|
+
if (!Number.isFinite(maximumChangedRate) || maximumChangedRate < 0 || maximumChangedRate > 1) {
|
|
61
|
+
throw new Error('label stability requires a changed-rate bound in [0, 1]');
|
|
62
|
+
}
|
|
63
|
+
const movement = measurePairedMovement(pairs);
|
|
64
|
+
const [low, high] = movement.changedWilson95;
|
|
65
|
+
return { ...movement, maximumChangedRate,
|
|
66
|
+
decision: high <= maximumChangedRate ? 'stable' : low > maximumChangedRate ? 'drift' : 'insufficient-evidence' };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=drift.js.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
3
|
+
import { CalibrationRegistry, calibrationArtifactDigest } from '../calibration/registry.js';
|
|
4
|
+
import { validateCaseInventory } from './manifest.js';
|
|
5
|
+
import { verifyQualificationSplits } from './quality.js';
|
|
6
|
+
const range = (prefix, from, to) => Array.from({ length: to - from + 1 }, (_, index) => `${prefix}${String(from + index).padStart(2, '0')}`);
|
|
7
|
+
export const DECISION_GATE_SUITES = {
|
|
8
|
+
'contract-suite-complete': { gate: 'G0', caseIds: ['C36'], evidenceIds: [
|
|
9
|
+
'CON-SCHEMA-01', 'CON-REJECT-01', 'CON-PIN-01', 'CON-VERSION-01', 'CON-CONVERT-01', 'CON-RESULT-01',
|
|
10
|
+
] },
|
|
11
|
+
'runtime-suite-complete': { gate: 'G1', caseIds: [
|
|
12
|
+
...range('C', 1, 12), ...range('C', 19, 23), 'C25', 'C26', 'C31', 'C35', 'C40', 'C41',
|
|
13
|
+
'TV01', 'TV02', 'TV03', 'TV04', 'TV05', 'TV06', 'TV07', 'TV08', 'TV09', 'TV11', 'TV12', 'TV18', 'TV22', 'TV23',
|
|
14
|
+
], evidenceIds: ['BCH-INVALID-ANSWER-01', 'CTX-64K-01', 'CTX-32K-01', 'CTX-QUALIFY-01'] },
|
|
15
|
+
'security-suite-complete': { gate: 'G2', caseIds: [
|
|
16
|
+
'C29', 'C30', 'C32', 'C34', 'C36', 'C39', 'TV19', 'TV21', 'TV24', 'TV25',
|
|
17
|
+
], evidenceIds: ['SEC-ADV-OVERRIDE-01', 'SEC-ADV-AUTHORITY-01', 'SEC-ADV-DELIMITER-01', 'SEC-ADV-FAKE-SYSTEM-01',
|
|
18
|
+
'SEC-EGRESS-01', 'SEC-RESPONSE-01', 'SEC-REQUEST-ID-01'],
|
|
19
|
+
amendments: ['M01', 'M02', 'M04', 'M05', 'M06', 'M07', 'M08', 'M09'] },
|
|
20
|
+
'fault-suite-complete': { gate: 'G4', caseIds: [
|
|
21
|
+
...range('C', 13, 18), 'C24', 'C27', 'C28', 'C33', 'C37', 'C38', 'C42', 'TV13', 'TV14', 'TV15', 'TV16', 'TV17',
|
|
22
|
+
], evidenceIds: ['RTY-STATUS-01', 'RTY-HEADER-01', 'RTY-DEADLINE-01', 'CAN-CALLER-01', 'CAN-DISPATCHED-01',
|
|
23
|
+
'CNC-ORDER-01', 'CNC-FAIR-01', 'CNC-QUEUE-01', 'CNC-CANCEL-01', 'CNC-RETRY-OWNER-01'],
|
|
24
|
+
amendments: ['M03', 'M10', 'M11'] },
|
|
25
|
+
'drift-suite-complete': { gate: 'G4', caseIds: ['TV10', 'TV20'], evidenceIds: [
|
|
26
|
+
'DRF-ALIAS-01', 'DRF-SHADOW-01', 'DRF-ACTIVE-PIN-01', 'DRF-PROMOTE-01', 'DRF-ROLLBACK-01', 'DRF-RETIRE-01',
|
|
27
|
+
'DRF-OUTPUT-01', 'DRF-POPULATION-01', 'DRF-LABEL-01', 'DRF-INSUFFICIENT-01',
|
|
28
|
+
] },
|
|
29
|
+
};
|
|
30
|
+
/** Flags backed by a checked, digest-pinned artifact copied into the run directory. */
|
|
31
|
+
export const GATE_ARTIFACT_SCHEMAS = {
|
|
32
|
+
'immutable-splits': 'decision-binary-benchmark-plan/v1',
|
|
33
|
+
'calibration-qualified': 'decision-calibration-artifact/v1',
|
|
34
|
+
'load-manifest-qualified': 'decision-load-result/v1',
|
|
35
|
+
'review-decision-recorded': 'decision-qualification-review/v1',
|
|
36
|
+
};
|
|
37
|
+
export const GATE_ARTIFACT_FLAGS = Object.keys(GATE_ARTIFACT_SCHEMAS);
|
|
38
|
+
/** Every name that the caller cannot assert through an evidence-check callback. */
|
|
39
|
+
export const DERIVED_GATE_EVIDENCE = [
|
|
40
|
+
'case-inventory-complete', ...Object.keys(DECISION_GATE_SUITES), 'evidence-hashes-verified',
|
|
41
|
+
'privacy-scan-clean', ...GATE_ARTIFACT_FLAGS,
|
|
42
|
+
];
|
|
43
|
+
const sha256Pattern = /^sha256:[0-9a-f]{64}$/;
|
|
44
|
+
const sha256 = (value) => `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
45
|
+
const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
+
const validDate = (value) => typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
47
|
+
/**
|
|
48
|
+
* Deterministic digest of the run's case outcomes and named evidence. Artifact
|
|
49
|
+
* digests are excluded (they bind durations), so a reviewer can sign the
|
|
50
|
+
* outcome set that the run must reproduce.
|
|
51
|
+
*/
|
|
52
|
+
export function qualificationOutcomesDigest(manifest) {
|
|
53
|
+
const outcomes = [...manifest.evidence].sort((a, b) => a.caseId.localeCompare(b.caseId)).map(item => ({
|
|
54
|
+
caseId: item.caseId, executable: item.executable, outcome: item.outcome,
|
|
55
|
+
testEvidenceIds: [...(item.testEvidenceIds ?? [])].sort(),
|
|
56
|
+
}));
|
|
57
|
+
return sha256(canonicalJson({ runId: manifest.runId, sourceCommit: manifest.sourceCommit, outcomes }));
|
|
58
|
+
}
|
|
59
|
+
export function loadManifestDigest(manifest) {
|
|
60
|
+
return sha256(canonicalJson(manifest));
|
|
61
|
+
}
|
|
62
|
+
/** True only when every suite case has executable, digest-bound, passing evidence and named IDs are proven. */
|
|
63
|
+
export function suiteSatisfied(manifest, suite) {
|
|
64
|
+
const byCase = new Map();
|
|
65
|
+
for (const item of manifest.evidence)
|
|
66
|
+
byCase.set(item.caseId, [...(byCase.get(item.caseId) ?? []), item]);
|
|
67
|
+
const proven = new Set();
|
|
68
|
+
for (const caseId of suite.caseIds) {
|
|
69
|
+
const items = byCase.get(caseId) ?? [];
|
|
70
|
+
const item = items[0];
|
|
71
|
+
if (items.length !== 1 || !item || !item.executable || item.outcome !== 'pass' || !item.artifact
|
|
72
|
+
|| !item.digest || !sha256Pattern.test(item.digest))
|
|
73
|
+
return false;
|
|
74
|
+
for (const id of item.testEvidenceIds ?? [])
|
|
75
|
+
proven.add(id);
|
|
76
|
+
}
|
|
77
|
+
return (suite.evidenceIds ?? []).every(id => proven.has(id));
|
|
78
|
+
}
|
|
79
|
+
function validSplitPlan(value) {
|
|
80
|
+
if (!record(value) || value.schemaVersion !== GATE_ARTIFACT_SCHEMAS['immutable-splits'])
|
|
81
|
+
return false;
|
|
82
|
+
const { digest, ...fields } = value;
|
|
83
|
+
try {
|
|
84
|
+
verifyQualificationSplits(fields.splits);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return typeof digest === 'string' && sha256Pattern.test(digest) && digest === sha256(JSON.stringify(fields))
|
|
90
|
+
&& sha256Pattern.test(String(fields.datasetDigest));
|
|
91
|
+
}
|
|
92
|
+
function validCalibration(value, context) {
|
|
93
|
+
if (!record(value) || value.schemaVersion !== GATE_ARTIFACT_SCHEMAS['calibration-qualified'])
|
|
94
|
+
return false;
|
|
95
|
+
const artifact = value;
|
|
96
|
+
const plan = context.accepted['immutable-splits'];
|
|
97
|
+
try {
|
|
98
|
+
new CalibrationRegistry().registerArtifact(artifact);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const { digest, ...payload } = artifact;
|
|
104
|
+
const at = Date.parse(context.manifest.generatedAt);
|
|
105
|
+
const effective = Date.parse(artifact.effectiveAt);
|
|
106
|
+
const expires = effective + artifact.profile.expiresAfterDays * 86_400_000;
|
|
107
|
+
return digest === calibrationArtifactDigest(payload) && artifact.approval.state === 'approved'
|
|
108
|
+
&& plan !== undefined && artifact.splitProvenance.hash === plan.digest
|
|
109
|
+
&& Number.isFinite(at) && effective <= at && at < expires
|
|
110
|
+
&& artifact.metrics.totalSamples >= artifact.profile.minimumTotalSamples
|
|
111
|
+
&& artifact.metrics.perSliceSamples >= artifact.profile.minimumPerSliceSamples
|
|
112
|
+
&& artifact.metrics.calibrationError <= artifact.profile.maximumCalibrationError
|
|
113
|
+
&& artifact.metrics.selectiveRisk <= artifact.profile.maximumSelectiveRisk;
|
|
114
|
+
}
|
|
115
|
+
function validLoadResult(value) {
|
|
116
|
+
if (!record(value) || value.schemaVersion !== GATE_ARTIFACT_SCHEMAS['load-manifest-qualified'])
|
|
117
|
+
return false;
|
|
118
|
+
const result = value;
|
|
119
|
+
if (!record(result.manifest) || result.manifest.schema !== 'decision-load-manifest/v1' || !record(result.manifest.bounds)
|
|
120
|
+
|| !record(result.observations) || !validDate(result.recordedAt) || result.mode !== result.manifest.mode)
|
|
121
|
+
return false;
|
|
122
|
+
let digest;
|
|
123
|
+
try {
|
|
124
|
+
digest = loadManifestDigest(result.manifest);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const bounds = Object.entries(result.manifest.bounds);
|
|
130
|
+
return digest === result.manifestDigest && bounds.length > 0
|
|
131
|
+
&& Object.keys(result.observations).length === bounds.length
|
|
132
|
+
&& bounds.every(([name, limit]) => {
|
|
133
|
+
const observed = result.observations[name];
|
|
134
|
+
return Number.isFinite(limit) && limit >= 0 && typeof observed === 'number' && Number.isFinite(observed)
|
|
135
|
+
&& observed >= 0 && observed <= limit;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function validReview(value, context) {
|
|
139
|
+
if (!record(value) || value.schemaVersion !== GATE_ARTIFACT_SCHEMAS['review-decision-recorded'])
|
|
140
|
+
return false;
|
|
141
|
+
const review = value;
|
|
142
|
+
return review.runId === context.manifest.runId && review.sourceCommit === context.manifest.sourceCommit
|
|
143
|
+
&& typeof review.reviewer === 'string' && review.reviewer.trim().length > 0 && review.decision === 'approve'
|
|
144
|
+
&& validDate(review.recordedAt) && review.outcomesDigest === qualificationOutcomesDigest(context.manifest);
|
|
145
|
+
}
|
|
146
|
+
/** Validates a parsed gate artifact against its schema, self-digest and the run it is attached to. */
|
|
147
|
+
export function validateGateArtifact(flag, value, context) {
|
|
148
|
+
switch (flag) {
|
|
149
|
+
case 'immutable-splits': return validSplitPlan(value);
|
|
150
|
+
case 'calibration-qualified': return validCalibration(value, context);
|
|
151
|
+
case 'load-manifest-qualified': return validLoadResult(value);
|
|
152
|
+
case 'review-decision-recorded': return validReview(value, context);
|
|
153
|
+
default: return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Computes every derived gate flag from the manifest; caller-supplied values for these names are ignored. */
|
|
157
|
+
export function deriveGateEvidence(manifest, proof = {}) {
|
|
158
|
+
const flags = {
|
|
159
|
+
'case-inventory-complete': validateCaseInventory(manifest.cases).length === 0,
|
|
160
|
+
'evidence-hashes-verified': proof.artifactsVerified === true,
|
|
161
|
+
'privacy-scan-clean': manifest.evidenceFlags['privacy-scan-clean'] === true,
|
|
162
|
+
};
|
|
163
|
+
for (const [name, suite] of Object.entries(DECISION_GATE_SUITES))
|
|
164
|
+
flags[name] = suiteSatisfied(manifest, suite);
|
|
165
|
+
for (const flag of GATE_ARTIFACT_FLAGS) {
|
|
166
|
+
const ref = manifest.gateArtifacts?.[flag];
|
|
167
|
+
flags[flag] = ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag]
|
|
168
|
+
&& typeof ref.artifact === 'string' && ref.artifact.length > 0 && sha256Pattern.test(ref.digest);
|
|
169
|
+
}
|
|
170
|
+
return flags;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=gate-evidence.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { expectedQualificationCaseIds, validateCaseInventory } from './manifest.js';
|
|
2
|
+
import { deriveGateEvidence, DERIVED_GATE_EVIDENCE } from './gate-evidence.js';
|
|
3
|
+
// Negative evidence always overrides a positive suite result. A release reviewer
|
|
4
|
+
// cannot waive these conditions by supplying passing evidence checks.
|
|
5
|
+
const BLOCKING_FINDINGS = {
|
|
6
|
+
G1: ['p0-correctness-failed', 'execution-uncertain'],
|
|
7
|
+
G2: ['privacy-denied'],
|
|
8
|
+
G3: ['calibration-data-missing'],
|
|
9
|
+
G6: ['p0-correctness-failed', 'execution-uncertain', 'privacy-denied', 'calibration-data-missing'],
|
|
10
|
+
};
|
|
11
|
+
export const DECISION_RELEASE_GATES = [
|
|
12
|
+
{ id: 'G0', title: 'contract and conformance', mandatory: true, requiredCaseIds: [], requiredEvidence: ['case-inventory-complete', 'contract-suite-complete'] },
|
|
13
|
+
{ id: 'G1', title: 'runtime and adapter correctness', mandatory: true, requiredCaseIds: [], requiredEvidence: ['runtime-suite-complete'] },
|
|
14
|
+
{ id: 'G2', title: 'privacy and security', mandatory: true, requiredCaseIds: [], requiredEvidence: ['privacy-scan-clean', 'security-suite-complete'] },
|
|
15
|
+
{ id: 'G3', title: 'calibration and held-out quality', mandatory: true, requiredCaseIds: [], requiredEvidence: ['immutable-splits', 'calibration-qualified'] },
|
|
16
|
+
{ id: 'G4', title: 'operational resilience', mandatory: true, requiredCaseIds: [], requiredEvidence: ['fault-suite-complete', 'drift-suite-complete'] },
|
|
17
|
+
{ id: 'G5', title: 'performance and resource bounds', mandatory: true, requiredCaseIds: [], requiredEvidence: ['load-manifest-qualified'] },
|
|
18
|
+
{ id: 'G6', title: 'release evidence and review', mandatory: true, requiredCaseIds: [], requiredEvidence: ['evidence-hashes-verified', 'review-decision-recorded'] },
|
|
19
|
+
];
|
|
20
|
+
function evaluateGate(definition, manifest, derived) {
|
|
21
|
+
const evidenceByCase = new Map(manifest.evidence.map(item => [item.caseId, item]));
|
|
22
|
+
const requiredCases = definition.id === 'G0'
|
|
23
|
+
? expectedQualificationCaseIds()
|
|
24
|
+
: definition.requiredCaseIds;
|
|
25
|
+
const missing = [];
|
|
26
|
+
const failed = [];
|
|
27
|
+
if (definition.id === 'G0') {
|
|
28
|
+
missing.push(...validateCaseInventory(manifest.cases).map(error => `inventory:${error}`));
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const known = new Set(expectedQualificationCaseIds());
|
|
31
|
+
for (const item of manifest.evidence) {
|
|
32
|
+
if (seen.has(item.caseId))
|
|
33
|
+
failed.push(`duplicate-evidence:${item.caseId}`);
|
|
34
|
+
if (!known.has(item.caseId))
|
|
35
|
+
failed.push(`unknown-evidence:${item.caseId}`);
|
|
36
|
+
seen.add(item.caseId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const caseId of requiredCases) {
|
|
40
|
+
const evidence = evidenceByCase.get(caseId);
|
|
41
|
+
if (!evidence || !evidence.executable || !evidence.artifact || !evidence.digest
|
|
42
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(evidence.digest)) {
|
|
43
|
+
missing.push(`case:${caseId}`);
|
|
44
|
+
}
|
|
45
|
+
else if (evidence.outcome !== 'pass') {
|
|
46
|
+
failed.push(`case:${caseId}:${evidence.outcome}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Derived names are recomputed from recorded evidence; a caller-set flag of
|
|
50
|
+
// the same name is ignored rather than trusted.
|
|
51
|
+
const derivedNames = new Set(DERIVED_GATE_EVIDENCE);
|
|
52
|
+
for (const key of definition.requiredEvidence) {
|
|
53
|
+
const value = derivedNames.has(key) ? derived[key] : manifest.evidenceFlags[key];
|
|
54
|
+
if (value !== true)
|
|
55
|
+
missing.push(`evidence:${key}`);
|
|
56
|
+
}
|
|
57
|
+
for (const key of BLOCKING_FINDINGS[definition.id] ?? []) {
|
|
58
|
+
if (manifest.evidenceFlags[key] === true)
|
|
59
|
+
failed.push(`finding:${key}`);
|
|
60
|
+
}
|
|
61
|
+
// Mandatory gates never turn absent evidence into a skip/pass. Skip is reserved
|
|
62
|
+
// for future explicitly optional gates.
|
|
63
|
+
const status = missing.length || failed.length ? (definition.mandatory ? 'fail' : 'skip') : 'pass';
|
|
64
|
+
return { id: definition.id, title: definition.title, status, missing: missing.sort(), failed: failed.sort() };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Structural evaluator. Suite, inventory and artifact-backed flags are derived
|
|
68
|
+
* from the manifest; `evidence-hashes-verified` is only true when the
|
|
69
|
+
* executable pipeline passes a proof after re-reading every artifact.
|
|
70
|
+
*/
|
|
71
|
+
export function evaluateQualification(manifest, definitions = DECISION_RELEASE_GATES, proof = {}) {
|
|
72
|
+
const derived = deriveGateEvidence(manifest, proof);
|
|
73
|
+
const gates = definitions.map(definition => evaluateGate(definition, manifest, derived));
|
|
74
|
+
const mandatoryFailed = gates.some((gate, index) => definitions[index]?.mandatory && gate.status !== 'pass');
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: 'decision-qualification-report/v1',
|
|
77
|
+
runId: manifest.runId,
|
|
78
|
+
generatedAt: manifest.generatedAt,
|
|
79
|
+
decision: mandatoryFailed ? 'HOLD' : 'PROMOTE',
|
|
80
|
+
gates,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=gates.js.map
|