@aiwg/cli 2026.10.10 → 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.
Files changed (156) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +78 -14
  2. package/agentic/code/providers/model-catalog.v1.json +62 -12
  3. package/dist/src/artifacts/binding-target-identity.js +51 -0
  4. package/dist/src/artifacts/checksum-manifest.js +12 -0
  5. package/dist/src/artifacts/index-builder.js +11 -13
  6. package/dist/src/artifacts/query-engine.js +5 -11
  7. package/dist/src/cli/handlers/doctor-help.js +32 -0
  8. package/dist/src/cli/handlers/gates.js +19 -18
  9. package/dist/src/cli/handlers/models-help.js +7 -1
  10. package/dist/src/cli/handlers/models.js +3 -3
  11. package/dist/src/cli/handlers/script-runner.js +8 -5
  12. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  13. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  14. package/dist/src/cli/handlers/sessions.js +287 -31
  15. package/dist/src/cli/handlers/skill-lint.js +10 -3
  16. package/dist/src/cli/handlers/steward-format.js +100 -0
  17. package/dist/src/cli/handlers/steward-models.js +167 -0
  18. package/dist/src/cli/handlers/steward.js +14 -248
  19. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  20. package/dist/src/cli/handlers/use.js +18 -28
  21. package/dist/src/cli/handlers/utilities.js +1 -24
  22. package/dist/src/config/aiwg-config.js +7 -0
  23. package/dist/src/config/authorization-cli.js +89 -0
  24. package/dist/src/config/cli-help.js +1 -0
  25. package/dist/src/config/cli.js +10 -1
  26. package/dist/src/config/worker-context.js +337 -0
  27. package/dist/src/decision/context-pruning.js +3 -2
  28. package/dist/src/decision/ensemble/contract.js +15 -4
  29. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  30. package/dist/src/decision/ensemble-study/score.js +4 -3
  31. package/dist/src/decision/feature-export.js +3 -9
  32. package/dist/src/decision/heldout/collector.js +2 -2
  33. package/dist/src/decision/issue-triage/runtime.js +3 -9
  34. package/dist/src/decision/qualification/gates.js +2 -1
  35. package/dist/src/decision/qualification/release.js +2 -3
  36. package/dist/src/decision/routing/report.js +2 -0
  37. package/dist/src/decision/sdlc-screening.js +3 -5
  38. package/dist/src/decision/sensitivity/comparative.js +5 -3
  39. package/dist/src/gates/driver.js +6 -77
  40. package/dist/src/gates/evaluate.js +4 -11
  41. package/dist/src/gates/floors.js +4 -4
  42. package/dist/src/gates/index.js +3 -3
  43. package/dist/src/gates/lattice.js +26 -0
  44. package/dist/src/gates/registry.js +2 -1
  45. package/dist/src/gates/report.js +19 -7
  46. package/dist/src/issues/audit-cli.js +120 -0
  47. package/dist/src/issues/audit-contract.js +64 -0
  48. package/dist/src/issues/audit-local.js +82 -0
  49. package/dist/src/issues/audit.js +220 -0
  50. package/dist/src/lint/rule-activation.js +15 -1
  51. package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
  52. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  53. package/dist/src/mission-protocol/codecs.js +22 -550
  54. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  55. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  56. package/dist/src/mission-protocol/types.js +2 -42
  57. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  58. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  59. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  60. package/dist/src/models/catalog-policy.js +280 -0
  61. package/dist/src/models/index.js +1 -1
  62. package/dist/src/models/model-capabilities.v1.json +78 -14
  63. package/dist/src/models/model-catalog.v1.json +62 -12
  64. package/dist/src/models/policy-resources.js +28 -0
  65. package/dist/src/models/provider-policy.js +10 -539
  66. package/dist/src/models/resolver.js +26 -8
  67. package/dist/src/models/router.js +15 -2
  68. package/dist/src/models/tiers.js +13 -0
  69. package/dist/src/models/types.js +0 -6
  70. package/dist/src/models/worker-ceilings.js +59 -0
  71. package/dist/src/models/worker-invocation-policy.js +243 -0
  72. package/dist/src/models/worker-plan.js +16 -20
  73. package/dist/src/models/worker-review-lineage.js +55 -0
  74. package/dist/src/models/worker-selection-policy.js +1 -1
  75. package/dist/src/models/wrapper-route.js +6 -3
  76. package/dist/src/policy/authorization.js +30 -1
  77. package/dist/src/policy/hitl-config.js +63 -0
  78. package/dist/src/policy/hitl-receipt.js +57 -0
  79. package/dist/src/policy/hitl-types.js +4 -0
  80. package/dist/src/policy/hitl-validation.js +39 -0
  81. package/dist/src/policy/hitl.js +117 -0
  82. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  83. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  84. package/dist/src/rules/activation.d.mts +1 -1
  85. package/dist/src/rules/activation.mjs +0 -2
  86. package/dist/src/rules/gate-coverage.js +51 -0
  87. package/dist/src/rules/gate-references.js +16 -0
  88. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  89. package/dist/src/serve/worker-command-request.js +21 -4
  90. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  91. package/dist/src/serve/worker-invocation.js +21 -10
  92. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  93. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  94. package/dist/src/sessions/adapters/grok-build.js +57 -15
  95. package/dist/src/sessions/adapters/grokbot.js +7 -0
  96. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  97. package/dist/src/sessions/adapters/hermes.js +42 -3
  98. package/dist/src/sessions/adapters/warp.js +1 -1
  99. package/dist/src/sessions/batch-import.js +44 -6
  100. package/dist/src/sessions/contracts.js +20 -1
  101. package/dist/src/sessions/coverage-preflight.js +7 -1
  102. package/dist/src/sessions/discovery-capability.js +38 -0
  103. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  104. package/dist/src/sessions/host-catalog.js +157 -0
  105. package/dist/src/sessions/image-blobs.js +141 -0
  106. package/dist/src/sessions/importer.js +9 -232
  107. package/dist/src/sessions/index.js +6 -0
  108. package/dist/src/sessions/intake-dir.js +71 -0
  109. package/dist/src/sessions/normalization.js +251 -0
  110. package/dist/src/sessions/refresh.js +25 -2
  111. package/dist/src/sessions/repository.js +164 -22
  112. package/dist/src/sessions/tail.js +91 -0
  113. package/dist/src/sessions/workspace-discovery.js +88 -9
  114. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  115. package/dist/src/skills/agent-skills.js +4 -23
  116. package/dist/src/skills/claude-surface-projection.js +25 -0
  117. package/dist/src/skills/deployer.js +8 -288
  118. package/dist/src/skills/deployment-policy.js +114 -0
  119. package/dist/src/skills/deployment-projection.js +152 -0
  120. package/dist/src/skills/deployment-types.js +20 -0
  121. package/dist/src/skills/exporter.js +8 -31
  122. package/dist/src/skills/provider-extensions.js +41 -0
  123. package/dist/src/skills/run.js +6 -0
  124. package/dist/src/skills/types.js +0 -8
  125. package/dist/src/skills/validator-resources.js +68 -0
  126. package/dist/src/skills/validator.js +6 -55
  127. package/dist/src/storage/cli-migrate-options.js +125 -0
  128. package/dist/src/storage/cli-migrate.js +111 -0
  129. package/dist/src/storage/cli.js +4 -279
  130. package/dist/src/storage/migration-adapter.js +79 -0
  131. package/dist/src/storage/migration-copy.js +108 -0
  132. package/dist/src/storage/migration-protocol.js +14 -197
  133. package/dist/src/storage/migration-records.js +186 -0
  134. package/dist/src/storage/migration-types.js +12 -0
  135. package/dist/src/tracker/forge-http.js +2 -1
  136. package/dist/src/tracker/issue-pages.js +110 -0
  137. package/package.json +7 -1
  138. package/schemas/mission-v1.schema.json +335 -0
  139. package/schemas/models/model-catalog.v1.schema.json +19 -1
  140. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  141. package/schemas/models/model-policy.v1.schema.json +1 -1
  142. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  143. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  144. package/tools/agents/providers/rule-delivery.mjs +7 -126
  145. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  146. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  147. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  148. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  149. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  150. package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
  151. package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
  152. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  153. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  154. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  155. package/tools/ralph-external/session-launcher.mjs +51 -96
  156. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -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 === 'ROLLBACK' || compromised ? 'ROLLBACK' : upstream === 'HOLD' || findingCount > 0 ? 'HOLD' : 'PROMOTE';
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 === 'ROLLBACK' || input.integrity.compromise_labels.length ? 'ROLLBACK' : 'HOLD' };
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 === 'ROLLBACK' || input.integrity.compromise_labels.length ? 'ROLLBACK' : 'HOLD';
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
- if (integrityRank(value.disposition) < integrityRank(value.upstreamDisposition)) {
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
- if (integrityRank(record.decision) < integrityRank(record.integrity.release_gate.decision)) {
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 === 'ROLLBACK'
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
- if (GATE_RANK[report.decision] < GATE_RANK[report.integrity.upstreamDecision]) {
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
- if (upstream === 'ROLLBACK')
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.release_gate.decision === 'ROLLBACK'
1139
- || input.integrity.integrity_state === 'compromised' || input.integrity.compromise_labels.length > 0);
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 === 'ROLLBACK' || input.integrity.integrity_state === 'compromised'
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
  }
@@ -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, the kind and the self-digest,
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 { readdir, readFile: readRuleFile, stat } = await import('node:fs/promises');
288
- const roots = [];
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
- const RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
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
- if (compromised)
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 = maxOutcome(packOutcome, upstreamCeiling, bindingCeiling);
681
+ const decision = ceilingDecision(packOutcome, upstreamCeiling, bindingCeiling);
689
682
  const body = {
690
683
  apiVersion: binding.apiVersion,
691
684
  kind: 'GateReport',
@@ -1,6 +1,6 @@
1
+ import { GATE_RANK } from './lattice.js';
1
2
  import { validateGateDocument } from './schema.js';
2
3
  import { splitPackId } from './registry.js';
3
- const OUTCOME_RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
4
4
  const OUTCOMES = ['PROMOTE', 'HOLD', 'ROLLBACK'];
5
5
  const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
6
6
  const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
@@ -22,7 +22,7 @@ export const INTEGRITY_CEILING_FLOOR_PACK_VERSION = '1.0.0';
22
22
  export function projectCeilingSatisfied(declared, configured) {
23
23
  if (configured === undefined)
24
24
  return true;
25
- return OUTCOME_RANK[declared ?? 'PROMOTE'] >= OUTCOME_RANK[configured];
25
+ return GATE_RANK[declared ?? 'PROMOTE'] >= GATE_RANK[configured];
26
26
  }
27
27
  /**
28
28
  * Resolves the effective project floors: the configured entries, verbatim.
@@ -64,7 +64,7 @@ export function assertCeilingDefaults(ceilings) {
64
64
  continue;
65
65
  if (!starValid)
66
66
  throw new Error(`gates.ceilings: per-study ceilings require the project-wide '*' default ceiling`);
67
- if (OUTCOME_RANK[ceiling] < OUTCOME_RANK[star]) {
67
+ if (GATE_RANK[ceiling] < GATE_RANK[star]) {
68
68
  throw new Error(`gates.ceilings.${study}: must tighten the project default ceiling ${String(star)}, not loosen it`);
69
69
  }
70
70
  }
@@ -124,7 +124,7 @@ export function validateGatesConfig(gates) {
124
124
  for (const [study, ceiling] of Object.entries(gates.ceilings)) {
125
125
  if (study !== '*' && typeof ceiling === 'string'
126
126
  && OUTCOMES.includes(ceiling)
127
- && OUTCOME_RANK[ceiling] < OUTCOME_RANK[star]) {
127
+ && GATE_RANK[ceiling] < GATE_RANK[star]) {
128
128
  errors.push(`gates.ceilings.${study}: must tighten the project default ceiling ${star}, not loosen it`);
129
129
  }
130
130
  }
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Gates capability phase 1 core (epic #2824): declarative gate packs, a pure deterministic
3
- * evaluator and digest-bound reports. Experimental and default-off: nothing in the
4
- * decision runtime imports this module, so existing behavior is unchanged.
3
+ * evaluator and digest-bound reports. Decision consumers share its outcome lattice.
5
4
  */
6
5
  export { GATES_API_VERSION, qualifyGateParameter } from './types.js';
7
6
  export { GateSchemaError, validateGateDocument } from './schema.js';
8
7
  export { GateRegistryError, GATE_NAMESPACES, splitPackId, validateResolvedPack, resolveThresholdDefault, assertGateTightens, applyGateExtends, composeGatePack, resolveGateBinding, expandFloorPacks, enforceProjectFloors, authoredGatePacksOf, gateProvidersOf, GateRegistry, } from './registry.js';
9
8
  export { INTEGRITY_CEILING_FLOOR_PACK_ID, INTEGRITY_CEILING_FLOOR_PACK_VERSION, assertCeilingDefaults, projectCeilingSatisfied, resolveProjectFloors, validateGatesConfig, } from './floors.js';
10
9
  export { GateEvaluationError, maxOutcome, gateStatusOutcome, sealUpstream, sealGateHoldout, evaluateGates, } from './evaluate.js';
11
- export { validateGateReport } from './report.js';
10
+ export { assertGateReportDecision, validateGateReport } from './report.js';
12
11
  export { createCoreProviderRegistry, evidenceProvider, pairedProvider, proportionProvider, scalarProvider, screeningProvider } from './providers/index.js';
13
12
  export { MetricProviderRegistry } from './providers/index.js';
14
13
  export { GATE_PACKS_DIR, GATE_PACK_MAX_BYTES, isGatePackFile, listGatePackFiles, readBundleGatePacksDeclaration, loadGatePackFile, originForPackDir, registerBundleGatePacks, resolveGatePacksDir, discoverShippedGatePacks, } from './discovery.js';
15
14
  export { buildGatesRegistry, validateGateFile, loadHoldoutFile, loadUpstreamFile, evaluateGatesFromFiles, installedAiwgRoot, listGatePacks, loadProjectFloorsForEvaluate, showGatePack, packsReferencedByRule, } from './driver.js';
16
15
  export { GateProviderError, computeProviderCodeDigest, deriveIsolatedSeed, loadGateBundleProviders, invokeBundleProvider, providerBindingPin, runIsolatedProvider, runIsolatedProviderSync, sealProviderSection, snapshotProviderDir, isLoaderIssuedProvider, isolationLaunchPlan, isolationSandboxStatus, parseIsolatedFrame, probeIsolationSandbox, isBundleProvidersEnabled, GATE_PROVIDER_MAX_FILE_BYTES, GATE_PROVIDER_MAX_FILES, GATE_PROVIDER_MAX_TOTAL_BYTES, GATE_PROVIDER_DEFAULT_TIMEOUT_MS, GATE_PROVIDER_MAX_TIMEOUT_MS, GATE_PROVIDER_DEFAULT_MAX_RECORDS, GATE_PROVIDER_MAX_OUTPUT_BYTES, } from './providers/loader.js';
16
+ export { GATE_RANK, ceilingDecision, assertDecisionCeiling, GateDecisionUpgradeError } from './lattice.js';
17
17
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ export const GATE_RANK = Object.freeze({ PROMOTE: 0, HOLD: 1, ROLLBACK: 2 });
2
+ /** The strictest outcome wins; an empty set has no restriction. */
3
+ export function maxOutcome(...outcomes) {
4
+ return outcomes.reduce((worst, outcome) => GATE_RANK[outcome] > GATE_RANK[worst] ? outcome : worst, 'PROMOTE');
5
+ }
6
+ export function ceilingDecision(decision, ...ceilings) {
7
+ return maxOutcome(decision, ...ceilings);
8
+ }
9
+ export class GateDecisionUpgradeError extends Error {
10
+ code = 'report-upgrades-component';
11
+ decision;
12
+ component;
13
+ constructor(decision, component) {
14
+ super(`Report decision differs from components: cannot upgrade ${component} to ${decision} (report-upgrades-component)`);
15
+ this.name = 'GateDecisionUpgradeError';
16
+ this.decision = decision;
17
+ this.component = component;
18
+ }
19
+ }
20
+ /** Validate a carried verdict independently of its digest or reproducibility checks. */
21
+ export function assertDecisionCeiling(decision, ...components) {
22
+ const component = maxOutcome(...components);
23
+ if (GATE_RANK[decision] < GATE_RANK[component])
24
+ throw new GateDecisionUpgradeError(decision, component);
25
+ }
26
+ //# sourceMappingURL=lattice.js.map
@@ -1,3 +1,4 @@
1
+ import { ceilingDecision } from './lattice.js';
1
2
  import { canonicalJson } from '../security/artifact-trust.js';
2
3
  import { artifactDigest } from '../decision/validate.js';
3
4
  import { validateGateDocument } from './schema.js';
@@ -428,7 +429,7 @@ export function assertGateTightens(parent, child, params) {
428
429
  else if (minimumAfter < minimumBefore) {
429
430
  fail(`gate ${child.id} may only raise minimumN`);
430
431
  }
431
- if (parent.onFail === 'ROLLBACK' && child.onFail !== 'ROLLBACK') {
432
+ if (ceilingDecision(child.onFail, parent.onFail) !== child.onFail) {
432
433
  fail(`gate ${child.id} may not demote onFail ROLLBACK -> HOLD`);
433
434
  }
434
435
  // Insufficient evidence always holds: ROLLBACK needs an observed blocking
@@ -1,6 +1,7 @@
1
+ import { assertDecisionCeiling, GateDecisionUpgradeError } from './lattice.js';
1
2
  import { canonicalJson } from '../security/artifact-trust.js';
2
3
  import { artifactDigest } from '../decision/validate.js';
3
- import { evaluateGates, maxOutcome } from './evaluate.js';
4
+ import { evaluateGates } from './evaluate.js';
4
5
  import { validateGateDocument } from './schema.js';
5
6
  /**
6
7
  * Validates a gate report against its trusted inputs. The report is re-derived
@@ -85,12 +86,14 @@ export function validateGateReport(report, input) {
85
86
  }
86
87
  if (expected.decision !== parsed.decision)
87
88
  reasons.push('report-decision-mismatch');
88
- const components = [
89
- ...parsed.gateEvidence.map(entry => entry.outcome),
90
- parsed.ceilings.packOutcome, parsed.ceilings.upstreamCeiling, parsed.ceilings.bindingCeiling,
91
- ];
92
- if (maxOutcome(parsed.decision, maxOutcome(...components)) !== parsed.decision) {
93
- reasons.push('report-upgrades-component');
89
+ try {
90
+ assertGateReportDecision(parsed);
91
+ assertDecisionCeiling(parsed.decision, input.upstream?.metadata.release_gate.decision ?? 'PROMOTE', input.binding.spec.ceiling ?? 'PROMOTE');
92
+ }
93
+ catch (error) {
94
+ if (!(error instanceof GateDecisionUpgradeError))
95
+ throw error;
96
+ return { valid: false, reasons: [...reasons, error.code], error };
94
97
  }
95
98
  return { valid: reasons.length === 0, reasons };
96
99
  }
@@ -99,4 +102,13 @@ function stripDigest(report) {
99
102
  void _dropped;
100
103
  return fields;
101
104
  }
105
+ /** Validate carried component ceilings even when no trusted replay context is available. */
106
+ export function assertGateReportDecision(report) {
107
+ const components = [
108
+ ...report.gateEvidence.map(entry => entry.outcome),
109
+ report.ceilings.packOutcome, report.ceilings.upstreamCeiling, report.ceilings.bindingCeiling,
110
+ report.upstream?.decision ?? 'PROMOTE',
111
+ ];
112
+ assertDecisionCeiling(report.decision, ...components);
113
+ }
102
114
  //# sourceMappingURL=report.js.map