eval-quality 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +17 -11
  3. package/corpus/dev/compile-seal-example/brief.json +1 -1
  4. package/corpus/dev/compile-seal-example/contract.json +1 -1
  5. package/corpus/dev/contracts/absent-collection-locations.json +1 -1
  6. package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
  7. package/corpus/dev/contracts/absent-success-indicator.json +1 -1
  8. package/corpus/dev/contracts/empty-channel-roles.json +1 -1
  9. package/corpus/dev/contracts/empty-collection-locations.json +1 -1
  10. package/corpus/dev/contracts/empty-request-shapes.json +1 -1
  11. package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
  12. package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
  13. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  14. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  15. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  16. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  17. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  18. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  19. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  20. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  21. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  22. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  23. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  24. package/corpus/dev/index.json +1 -1
  25. package/dist/application/index.d.ts +2 -0
  26. package/dist/application/index.js +1 -0
  27. package/dist/application/score.d.ts +48 -0
  28. package/dist/application/score.js +186 -0
  29. package/dist/cli/arguments.d.ts +7 -3
  30. package/dist/cli/arguments.js +73 -5
  31. package/dist/cli/exit-codes.d.ts +6 -2
  32. package/dist/cli/exit-codes.js +8 -9
  33. package/dist/cli/main.js +9 -0
  34. package/dist/cli/render.d.ts +1 -1
  35. package/dist/cli/render.js +6 -4
  36. package/dist/cli/run.d.ts +13 -3
  37. package/dist/cli/run.js +136 -16
  38. package/dist/core/canonical/canonicalize.d.ts +12 -0
  39. package/dist/core/canonical/canonicalize.js +18 -4
  40. package/dist/core/compile/bindings.d.ts +70 -0
  41. package/dist/core/compile/bindings.js +317 -0
  42. package/dist/core/compile/compile.d.ts +5 -0
  43. package/dist/core/compile/compile.js +9 -0
  44. package/dist/core/compile/expression-legality.d.ts +49 -0
  45. package/dist/core/compile/expression-legality.js +105 -31
  46. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  47. package/dist/core/compile/forbidden-inputs.js +16 -3
  48. package/dist/core/compile/interface-inventory.d.ts +23 -1
  49. package/dist/core/compile/interface-inventory.js +32 -3
  50. package/dist/core/compile/reachability.d.ts +18 -1
  51. package/dist/core/compile/reachability.js +46 -1
  52. package/dist/core/compile/sensitivity-witness.d.ts +1 -1
  53. package/dist/core/compile/sensitivity-witness.js +1 -1
  54. package/dist/core/emit/emit.d.ts +9 -0
  55. package/dist/core/emit/emit.js +214 -0
  56. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  57. package/dist/core/emit/private-artifact-digest.js +31 -0
  58. package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
  59. package/dist/core/evaluate/evidence-resolution.js +4 -1
  60. package/dist/core/failure-codes.d.ts +2 -2
  61. package/dist/core/failure-codes.js +4 -2
  62. package/dist/core/ingest/conditions.d.ts +201 -0
  63. package/dist/core/ingest/conditions.js +48 -0
  64. package/dist/core/ingest/index.d.ts +13 -0
  65. package/dist/core/ingest/index.js +11 -0
  66. package/dist/core/ingest/ingest.d.ts +68 -0
  67. package/dist/core/ingest/ingest.js +310 -0
  68. package/dist/core/lineage/stage-table.d.ts +21 -4
  69. package/dist/core/lineage/stage-table.js +35 -3
  70. package/dist/core/preflight/witness-evidence.js +10 -0
  71. package/dist/core/schemas/artifact.d.ts +385 -14
  72. package/dist/core/schemas/constraint-ledger.js +40 -0
  73. package/dist/core/schemas/defect-signature.d.ts +273 -0
  74. package/dist/core/schemas/defect-signature.js +127 -0
  75. package/dist/core/schemas/eval-contract.d.ts +33 -0
  76. package/dist/core/schemas/eval-contract.js +14 -0
  77. package/dist/core/schemas/evidence-artifact.d.ts +99 -19
  78. package/dist/core/schemas/evidence-artifact.js +64 -8
  79. package/dist/core/schemas/plan.d.ts +112 -0
  80. package/dist/core/schemas/plan.js +67 -2
  81. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  82. package/dist/core/schemas/probe-qualification.js +106 -0
  83. package/dist/core/schemas/probe.d.ts +312 -0
  84. package/dist/core/schemas/probe.js +6 -2
  85. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  86. package/dist/core/schemas/scoring-policy.js +6 -1
  87. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  88. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  89. package/dist/core/schemas/sealed-run-record.d.ts +30 -3
  90. package/dist/core/schemas/sealed-run-record.js +37 -5
  91. package/dist/core/score/binding-order.d.ts +31 -0
  92. package/dist/core/score/binding-order.js +88 -0
  93. package/dist/core/score/bindings.d.ts +93 -0
  94. package/dist/core/score/bindings.js +297 -0
  95. package/dist/core/score/ladder-table.d.ts +23 -0
  96. package/dist/core/score/ladder-table.js +109 -0
  97. package/dist/core/score/ladder.d.ts +229 -0
  98. package/dist/core/score/ladder.js +464 -0
  99. package/dist/core/score/mode-agreement.d.ts +30 -0
  100. package/dist/core/score/mode-agreement.js +16 -0
  101. package/dist/core/score/outcome-table.d.ts +17 -0
  102. package/dist/core/score/outcome-table.js +172 -0
  103. package/dist/core/score/outcome.d.ts +466 -0
  104. package/dist/core/score/outcome.js +562 -0
  105. package/dist/core/score/qualification.d.ts +83 -0
  106. package/dist/core/score/qualification.js +615 -0
  107. package/dist/core/score/quotation.d.ts +79 -0
  108. package/dist/core/score/quotation.js +201 -0
  109. package/dist/core/score/reduce-trials.d.ts +80 -0
  110. package/dist/core/score/reduce-trials.js +90 -0
  111. package/dist/core/score/score.d.ts +72 -0
  112. package/dist/core/score/score.js +581 -0
  113. package/dist/core/score/selection.d.ts +80 -0
  114. package/dist/core/score/selection.js +98 -0
  115. package/dist/core/score/strength.d.ts +49 -0
  116. package/dist/core/score/strength.js +193 -0
  117. package/dist/core/score/witness.d.ts +117 -0
  118. package/dist/core/score/witness.js +315 -0
  119. package/dist/core/seal/derived-reference.d.ts +1 -1
  120. package/dist/core/seal/derived-reference.js +236 -28
  121. package/dist/core/seal/seal.js +8 -3
  122. package/dist/core/stage-contracts.d.ts +62 -2
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/testing/index.d.ts +2 -0
  126. package/dist/testing/index.js +1 -0
  127. package/package.json +12 -3
  128. package/schemas/eval-contract.schema.json +100 -2
  129. package/schemas/evidence-artifact.schema.json +96 -9
  130. package/schemas/probe.schema.json +508 -3
  131. package/schemas/scoring-policy.schema.json +8 -1
  132. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  133. package/schemas/sealed-run-record.schema.json +19 -2
@@ -1,10 +1,32 @@
1
1
  import type { EvalContract } from '../schemas/eval-contract.ts';
2
2
  /** Rejects permitted interface kinds that this contract version cannot run. */
3
3
  export declare function checkInterfaceKind(contract: EvalContract): void;
4
+ /**
5
+ * The transport identity AD-40 resolves a defect signature against: the method
6
+ * and the path template with parameter names erased, so `/notes/{id}` and
7
+ * `/notes/{noteId}` share one signature. Takes the two fields rather than an
8
+ * `Operation`, since AD-40's corpus-side signature declares the same pair and
9
+ * must produce the same string from it or the comparison is not a comparison.
10
+ */
11
+ export declare function operationSignature(operation: {
12
+ readonly method: string;
13
+ readonly pathTemplate: string;
14
+ }): string;
4
15
  /** Finds duplicate method and path signatures across the full inventory. */
5
16
  export declare function checkDuplicateOperationSignature(contract: EvalContract): void;
6
17
  /**
7
- * Checks each binding key against its operation's request shape.
18
+ * Checks each binding key against its operation's request shape, and each
19
+ * `{ principal }` value against the contract's declared principals.
8
20
  * Steps with unresolved operation IDs belong to a separate cross-field rule.
21
+ *
22
+ * The two conditions share one code and are not the same predicate: the key
23
+ * case is an input the contract did not declare, the principal case has a
24
+ * declared key whose referenced name the contract did not declare. AD-5's row
25
+ * is widened to say so. Placing the principal case here rather than in a root
26
+ * Zod refinement is deliberate: a constraint reaching from
27
+ * `interactionPlan[].inputBinding` into `testData.principals` cannot survive
28
+ * the export, and the published-schema differential sweep synthesises a
29
+ * `{ principal }` union-branch witness naming no declared principal, which a
30
+ * refinement would reject and ajv would accept.
9
31
  */
10
32
  export declare function checkUndeclaredMandatoryInput(contract: EvalContract): void;
@@ -16,12 +16,22 @@ export function checkInterfaceKind(contract) {
16
16
  /** Erases parameter names so equivalent path templates share a signature. */
17
17
  const PARAMETER_SEGMENT_PATTERN = /\{[A-Za-z0-9_-]+\}/g;
18
18
  const erase = (pathTemplate) => pathTemplate.replace(PARAMETER_SEGMENT_PATTERN, '{}');
19
+ /**
20
+ * The transport identity AD-40 resolves a defect signature against: the method
21
+ * and the path template with parameter names erased, so `/notes/{id}` and
22
+ * `/notes/{noteId}` share one signature. Takes the two fields rather than an
23
+ * `Operation`, since AD-40's corpus-side signature declares the same pair and
24
+ * must produce the same string from it or the comparison is not a comparison.
25
+ */
26
+ export function operationSignature(operation) {
27
+ return `${operation.method} ${erase(operation.pathTemplate)}`;
28
+ }
19
29
  /** Finds duplicate method and path signatures across the full inventory. */
20
30
  export function checkDuplicateOperationSignature(contract) {
21
31
  const seen = new Map();
22
32
  for (const iface of contract.permittedInterfaces) {
23
33
  for (const operation of iface.operations) {
24
- const signature = `${operation.method} ${erase(operation.pathTemplate)}`;
34
+ const signature = operationSignature(operation);
25
35
  const collision = seen.get(signature);
26
36
  if (collision !== undefined) {
27
37
  throw new StructuralFailure('duplicate-operation-signature', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}]`, `collides with permittedInterfaces[logicalId=${collision.logicalId}].operations[operationId=${collision.operation.operationId}] after parameter-name erasure ("${signature}") (AD-19, AD-40)`);
@@ -31,11 +41,23 @@ export function checkDuplicateOperationSignature(contract) {
31
41
  }
32
42
  }
33
43
  /**
34
- * Checks each binding key against its operation's request shape.
44
+ * Checks each binding key against its operation's request shape, and each
45
+ * `{ principal }` value against the contract's declared principals.
35
46
  * Steps with unresolved operation IDs belong to a separate cross-field rule.
47
+ *
48
+ * The two conditions share one code and are not the same predicate: the key
49
+ * case is an input the contract did not declare, the principal case has a
50
+ * declared key whose referenced name the contract did not declare. AD-5's row
51
+ * is widened to say so. Placing the principal case here rather than in a root
52
+ * Zod refinement is deliberate: a constraint reaching from
53
+ * `interactionPlan[].inputBinding` into `testData.principals` cannot survive
54
+ * the export, and the published-schema differential sweep synthesises a
55
+ * `{ principal }` union-branch witness naming no declared principal, which a
56
+ * refinement would reject and ajv would accept.
36
57
  */
37
58
  export function checkUndeclaredMandatoryInput(contract) {
38
59
  const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
60
+ const principals = new Set(Object.keys(contract.testData.principals ?? {}));
39
61
  for (const step of contract.interactionPlan) {
40
62
  const operation = index.operationOf(step.operationId);
41
63
  if (operation === undefined)
@@ -46,8 +68,15 @@ export function checkUndeclaredMandatoryInput(contract) {
46
68
  continue;
47
69
  const { requiredKeys, permittedKeys } = operation.requestShape[channel];
48
70
  for (const key of Object.keys(binding)) {
71
+ const path = `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${channel}[${JSON.stringify(key)}]`;
49
72
  if (!requiredKeys.includes(key) && !permittedKeys.includes(key)) {
50
- throw new StructuralFailure('undeclared-mandatory-input', `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${channel}[${JSON.stringify(key)}]`, `operation "${operation.operationId}" declares "${key}" in neither requiredKeys nor permittedKeys of its ${channel} channel (AD-4)`);
73
+ throw new StructuralFailure('undeclared-mandatory-input', path, `operation "${operation.operationId}" declares "${key}" in neither requiredKeys nor permittedKeys of its ${channel} channel (AD-4)`);
74
+ }
75
+ const value = binding[key];
76
+ if (value !== undefined &&
77
+ 'principal' in value &&
78
+ !principals.has(value.principal)) {
79
+ throw new StructuralFailure('undeclared-mandatory-input', path, `binds the principal "${value.principal}", which testData.principals does not declare (AD-4, AD-19)`);
51
80
  }
52
81
  }
53
82
  }
@@ -1,6 +1,19 @@
1
1
  import type { EvalContract } from '../schemas/eval-contract.ts';
2
+ import type { Expression } from '../schemas/expression.ts';
3
+ import type { Operation } from '../schemas/interface.ts';
2
4
  import { type PlanIndex } from '../seal/plan-index.ts';
3
5
  export declare function checkBoundElementScope(contract: EvalContract): void;
6
+ /** `checkBoundElementScope` over one bare `Expression`. */
7
+ export declare function checkExpressionBoundElementScope(expression: Expression, artifactPath: string): void;
8
+ /**
9
+ * Visits every `{ pointer }` operand of one bare `Expression`, which a
10
+ * probe-side legality pass needs for the rules a contract has no equivalent of:
11
+ * that every pointer roots at the reserved step identifier, and which evidence
12
+ * channels the condition actually names.
13
+ */
14
+ export declare function forEachExpressionPointer(expression: Expression, visit: (pointer: string, path: string) => void): void;
15
+ /** `checkEvidenceReachability` over one bare `Expression` and one operation. */
16
+ export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation: Operation): void;
4
17
  type ReachabilityResult = {
5
18
  readonly reachable: true;
6
19
  } | {
@@ -9,7 +22,11 @@ type ReachabilityResult = {
9
22
  };
10
23
  /**
11
24
  * Non-throwing core that `checkEvidenceReachability` wraps; also called
12
- * directly by tests.
25
+ * directly by tests. The two lines below are the whole of what ties this to a
26
+ * declared interaction plan; everything past them reads the operation alone,
27
+ * which is what `evaluateReachabilityAgainstOperation` exposes for a probe-side
28
+ * condition, whose one step identifier is reserved and resolves to the
29
+ * signature's home operation with no plan in sight.
13
30
  */
14
31
  export declare function evaluatePointerReachability(pointer: string, index: PlanIndex): ReachabilityResult;
15
32
  /** `unreachable-check-evidence`: an interaction-rooted pointer the declared interfaces cannot produce. */
@@ -60,6 +60,34 @@ export function checkBoundElementScope(contract) {
60
60
  }
61
61
  });
62
62
  }
63
+ /** `checkBoundElementScope` over one bare `Expression`. */
64
+ export function checkExpressionBoundElementScope(expression, artifactPath) {
65
+ visitExpression(expression, '', false, (site) => {
66
+ if (site.pointer.startsWith('@') && !site.insideQuantifier) {
67
+ throw new StructuralFailure('malformed-operator-expression', `${artifactPath}${site.path}`, `bound-element pointer "${site.pointer}" appears outside any quantifier's predicate; "@/" binds only inside a quantifier (AD-26)`);
68
+ }
69
+ });
70
+ }
71
+ /**
72
+ * Visits every `{ pointer }` operand of one bare `Expression`, which a
73
+ * probe-side legality pass needs for the rules a contract has no equivalent of:
74
+ * that every pointer roots at the reserved step identifier, and which evidence
75
+ * channels the condition actually names.
76
+ */
77
+ export function forEachExpressionPointer(expression, visit) {
78
+ visitExpression(expression, '', false, (site) => {
79
+ visit(site.pointer, site.path);
80
+ });
81
+ }
82
+ /** `checkEvidenceReachability` over one bare `Expression` and one operation. */
83
+ export function checkExpressionEvidenceReachability(expression, artifactPath, operation) {
84
+ visitExpression(expression, '', false, (site) => {
85
+ const result = evaluateReachabilityAgainstOperation(site.pointer, operation);
86
+ if (!result.reachable) {
87
+ throw new StructuralFailure('unreachable-check-evidence', `${artifactPath}${site.path}`, `"${site.pointer}" ${result.reason}`);
88
+ }
89
+ });
90
+ }
63
91
  const reachable = () => ({ reachable: true });
64
92
  const unreachable = (reason) => ({
65
93
  reachable: false,
@@ -81,7 +109,11 @@ function descendsIntoDeclaredScalar(types, tail, firstToken) {
81
109
  }
82
110
  /**
83
111
  * Non-throwing core that `checkEvidenceReachability` wraps; also called
84
- * directly by tests.
112
+ * directly by tests. The two lines below are the whole of what ties this to a
113
+ * declared interaction plan; everything past them reads the operation alone,
114
+ * which is what `evaluateReachabilityAgainstOperation` exposes for a probe-side
115
+ * condition, whose one step identifier is reserved and resolves to the
116
+ * signature's home operation with no plan in sight.
85
117
  */
86
118
  export function evaluatePointerReachability(pointer, index) {
87
119
  // A `@/` pointer is relative to a bound element. Nothing declared to
@@ -97,6 +129,19 @@ export function evaluatePointerReachability(pointer, index) {
97
129
  if (operation === undefined) {
98
130
  return unreachable(`names step "${target.stepId}", which names operation "${step.operationId}", not declared by any permitted interface`);
99
131
  }
132
+ return evaluateReachabilityAgainstOperation(pointer, operation);
133
+ }
134
+ /**
135
+ * The same rules, against an operation the caller already resolved. Without
136
+ * this check a probe-side condition addressing an undeclared channel or key
137
+ * resolves absent, every comparison over it resolves `false`, and the probe
138
+ * reports its defect as never triggered — a silently passing run on a signature
139
+ * that was never writable.
140
+ */
141
+ function evaluateReachabilityAgainstOperation(pointer, operation) {
142
+ if (pointer.startsWith('@'))
143
+ return reachable();
144
+ const target = parseEvidenceTarget(pointer);
100
145
  if (target.channel === 'stdout' || target.channel === 'stderr') {
101
146
  // stdout/stderr are always bare strings. A non-empty tail proves the
102
147
  // pointer unreachable.
@@ -4,7 +4,7 @@
4
4
  * execute it, and that its leg identifiers are distinct from each other and
5
5
  * from every interaction-plan step.
6
6
  *
7
- * No new AD-5 code is minted. The registry is closed at twenty-one and
7
+ * No new AD-5 code is minted. The registry is closed at twenty-three and
8
8
  * `check:ad5-registry` pins it against the spine, so each defect below takes the
9
9
  * code that already names it. Two rows stretch that reading: a leg-id equality
10
10
  * and a leg-id/step-id collision are identifier collisions, and
@@ -4,7 +4,7 @@
4
4
  * execute it, and that its leg identifiers are distinct from each other and
5
5
  * from every interaction-plan step.
6
6
  *
7
- * No new AD-5 code is minted. The registry is closed at twenty-one and
7
+ * No new AD-5 code is minted. The registry is closed at twenty-three and
8
8
  * `check:ad5-registry` pins it against the spine, so each defect below takes the
9
9
  * code that already names it. Two rows stretch that reading: a leg-id equality
10
10
  * and a leg-id/step-id collision are identifier collisions, and
@@ -0,0 +1,9 @@
1
+ import type { ScoredOutcomesAndVerdict } from '../score/score.ts';
2
+ import type { EmitStage } from '../stage-contracts.ts';
3
+ /**
4
+ * The stage. Builds `scoringVersionInputs`/`scoringVersion`/
5
+ * `comparabilityKey`/the strength block/the remediation block once, shared
6
+ * between the two mode branches, then the mode-discriminated artifact
7
+ * literal, then calls `checkModeAgreement` immediately before returning.
8
+ */
9
+ export declare const emit: EmitStage<ScoredOutcomesAndVerdict>;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * AD-24's emit stage: `score`'s widened product plus the three caller-
3
+ * attested digests AD-11 fixes, minting the `EvidenceArtifact` this package
4
+ * has never built until now. Pure per AD-1/AD-2, and total the way every
5
+ * other stage in this package is: nothing here throws for a domain input.
6
+ *
7
+ * `comparabilityKey`, `strength`, and `remediation` are lifted verbatim from
8
+ * `scripts/worked-example-target.ts`'s own hand-assembly (its former
9
+ * lines 1432-1487), the field-by-field precedent this module generalises
10
+ * from one caller-assembled record to `score`'s own widened product.
11
+ */
12
+ import { digestArtifact } from '../canonical/digest.js';
13
+ import { freezeArtifact } from '../lineage/freeze.js';
14
+ import { EvidenceArtifact, } from '../schemas/evidence-artifact.js';
15
+ import { checkModeAgreement } from '../score/mode-agreement.js';
16
+ import { buildStrengthVector } from '../score/strength.js';
17
+ const SCORING_POLICY_ARTIFACT_PATH = 'ScoringPolicy';
18
+ const SCORING_VERSION_INPUTS_ARTIFACT_PATH = 'ScoringVersionInputs';
19
+ const COMPARABILITY_KEY_ARTIFACT_PATH = 'ComparabilityKey';
20
+ /**
21
+ * `mode` joins AD-11's three caller-attested digest names as a fourth
22
+ * member: like them, it has no artifact source anywhere in this pipeline and
23
+ * can only ever be caller-supplied, never re-derived by `score`.
24
+ */
25
+ const CALLER_ATTESTED_INPUTS = ['corpusDigest', 'fixtureDigest', 'evaluatorConfigurationDigest', 'mode'];
26
+ /**
27
+ * The stage. Builds `scoringVersionInputs`/`scoringVersion`/
28
+ * `comparabilityKey`/the strength block/the remediation block once, shared
29
+ * between the two mode branches, then the mode-discriminated artifact
30
+ * literal, then calls `checkModeAgreement` immediately before returning.
31
+ */
32
+ export const emit = (scored, corpusDigest, fixtureDigest, evaluatorConfigurationDigest) => {
33
+ const scoringPolicyDigest = digestArtifact(scored.policy, SCORING_POLICY_ARTIFACT_PATH);
34
+ // AD-7's declared key: the scoring policy digest plus the corpus digest
35
+ // restricted to the probes both results cover -- here, the admitted probe
36
+ // identifiers, sorted so array order never enters the digest.
37
+ const comparabilityKey = digestArtifact({
38
+ scoringPolicyDigest,
39
+ probeIds: scored.sealedProbes.admitted
40
+ .map((entry) => entry.probe.probeId)
41
+ .sort(),
42
+ }, COMPARABILITY_KEY_ARTIFACT_PATH);
43
+ const excludedProbeIds = scored.sealedProbes.rejected.map((entry) => entry.probe.probeId);
44
+ const { trials, outcomes: scoredOutcomes } = scored.assessment.outcomeState;
45
+ const trialCount = `${trials.completed} completed trial${trials.completed === 1 ? '' : 's'}`;
46
+ const unreachedOracles = scoredOutcomes.filter((outcome) => outcome.resolution.state === 'unreached');
47
+ // AD-21: a run that resolved `unreached` or completed fewer trials than
48
+ // the policy's declared minimum marks the vector non-comparable rather
49
+ // than silently comparing it.
50
+ const comparable = trials.completed >= trials.declaredMinimum && unreachedOracles.length === 0;
51
+ const strengthNote = [
52
+ `${scored.sealedProbes.admitted.length} admitted probe over ${trialCount}.`,
53
+ trials.completed < trials.declaredMinimum
54
+ ? `Below the declared minimum of ${trials.declaredMinimum}.`
55
+ : null,
56
+ unreachedOracles.length > 0
57
+ ? `${unreachedOracles.map((outcome) => outcome.oracleId).join(', ')} resolved unreached.`
58
+ : null,
59
+ comparable ? null : 'The vector is reported and marked non-comparable.',
60
+ ]
61
+ .filter((part) => part !== null)
62
+ .join(' ');
63
+ const vector = buildStrengthVector(scored.sealedProbes.admitted, new Map([[scored.probe.probeId, scored.trialSetResult]]));
64
+ // `scored.ladder.verdict` is `Verdict | null`: `null` is AD-21's Invalid
65
+ // rung, which "never becomes a contract verdict" -- a run reaching that
66
+ // rung is the caller's own signal to stop before minting an artifact at
67
+ // all, exactly as `scripts/worked-example-target.ts`'s own
68
+ // `if (ladder.verdict === null) fail(...)` guard, immediately before its
69
+ // call into this stage, already enforces. `emit` throws nothing for a
70
+ // domain input (the one throw this stage carries is the mode-agreement
71
+ // check below), so a verdict-less ladder reaching this point is a
72
+ // precondition violation the same way `seal()`'s own null-direction
73
+ // guard is one call earlier in the pipeline: trusted rather than
74
+ // re-checked a second time.
75
+ const verdict = scored.ladder.verdict;
76
+ const commonFields = {
77
+ schemaVersion: 3,
78
+ parentDigest: null,
79
+ // v0 mints no revision path for an evidence artifact: every `emit`
80
+ // call is a lineage root, matching `seal.ts`'s own root-artifact
81
+ // precedent for the one other `lineage: 'mints'` stage with a built
82
+ // module.
83
+ revisionCount: 0,
84
+ runId: scored.runId,
85
+ comparabilityKey,
86
+ excludedProbeIds,
87
+ exitCode: scored.ladder.exitCode,
88
+ verdictBasis: [...scored.ladder.basis],
89
+ callerAttestedInputs: [...CALLER_ATTESTED_INPUTS],
90
+ trials,
91
+ outcomes: [...scored.outcomes],
92
+ uncitedFindings: [...scored.uncitedFindings],
93
+ coverageGaps: [...scored.assessment.coverageGaps],
94
+ strength: {
95
+ denominator: `unique qualified probe identifiers exercised per class, across ${trialCount}`,
96
+ // `matchProbeWitness` is the only reachable producer of a
97
+ // `Strength.basis` value and it always returns `'measured'`;
98
+ // `'reconstructed'`'s one producer is dead code in v0. No live
99
+ // second value to select between, so this is a constant.
100
+ basis: 'measured',
101
+ vector,
102
+ comparable,
103
+ note: strengthNote,
104
+ },
105
+ remediation: {
106
+ // AD-12's REMEDIATION count, distinct from this artifact's own root
107
+ // `revisionCount` above; the two are named identically only because
108
+ // they nest at different depths.
109
+ revisionCount: scored.contract.revisionCount,
110
+ cap: scored.policy.remediationCap,
111
+ // AD-12 has the package only validate the cap; nothing enforces it,
112
+ // so there is no second legal value.
113
+ capSource: 'caller-attested',
114
+ // `remediationState` is already declared `LineageChain` (`ladder.ts`):
115
+ // reused from the assessment, never recomputed here.
116
+ lineageChain: scored.assessment.remediationState,
117
+ },
118
+ };
119
+ /**
120
+ * Runs both of this stage's checks, then freezes and returns. AD-29: a
121
+ * stage freezes the artifact it owns, matching `seal.ts`/
122
+ * `preflight/reduce.ts`'s own precedent for the other two minting stages.
123
+ * Both checks fire only on a precondition violation, never a domain
124
+ * input: neither is reachable through any path this module's own
125
+ * construction takes.
126
+ */
127
+ const finalize = (artifact) => {
128
+ const agreement = checkModeAgreement({ mode: scored.assessment.mode }, { mode: artifact.mode });
129
+ if (!agreement.agrees) {
130
+ // Reachable only through a type-system bypass: `artifact.mode` is
131
+ // stamped from `scored.assessment.mode` two lines above in every
132
+ // branch this function builds, so the two can disagree only if a
133
+ // caller assembled `scored` from two independently-sourced values
134
+ // -- the future caller this check exists for, mirroring
135
+ // `reduceTrialSet`'s own `TypeError`-for-bypass-only-input
136
+ // precedent.
137
+ throw new TypeError(`emit(): assembled an artifact whose mode ("${agreement.artifactMode}") disagrees with the assessment mode ("${agreement.recordMode}") it was built from`);
138
+ }
139
+ return freezeArtifact(validateAssembledArtifact(artifact));
140
+ };
141
+ if (scored.assessment.mode === 'production') {
142
+ const scoringVersionInputs = {
143
+ contractSchemaVersion: scored.contract.schemaVersion,
144
+ corpusDigest,
145
+ fixtureDigest,
146
+ evaluatorConfigurationDigest,
147
+ scoringPolicyDigest,
148
+ mode: 'production',
149
+ };
150
+ const artifact = {
151
+ ...commonFields,
152
+ mode: 'production',
153
+ scoringVersionInputs,
154
+ scoringVersion: digestArtifact(scoringVersionInputs, SCORING_VERSION_INPUTS_ARTIFACT_PATH),
155
+ productionVerdict: verdict,
156
+ };
157
+ return finalize(artifact);
158
+ }
159
+ const scoringVersionInputs = {
160
+ contractSchemaVersion: scored.contract.schemaVersion,
161
+ corpusDigest,
162
+ fixtureDigest,
163
+ evaluatorConfigurationDigest,
164
+ scoringPolicyDigest,
165
+ mode: 'contract-scoring',
166
+ };
167
+ const artifact = {
168
+ ...commonFields,
169
+ mode: 'contract-scoring',
170
+ scoringVersionInputs,
171
+ scoringVersion: digestArtifact(scoringVersionInputs, SCORING_VERSION_INPUTS_ARTIFACT_PATH),
172
+ contractVerdict: verdict,
173
+ uncitedFindingGaps: [...scored.assessment.uncitedDefectFindings],
174
+ systemRecommendationRecorded: scored.assessment.systemRecommendationRecorded,
175
+ systemRecommendationNote: scored.assessment.systemRecommendationNote,
176
+ };
177
+ return finalize(artifact);
178
+ };
179
+ /**
180
+ * The runtime backstop `finalize()` calls before freezing: TypeScript
181
+ * guarantees this module's own object literals match `EvidenceArtifact`'s
182
+ * shape at compile time, except at the one place that casts past it
183
+ * (`scored.ladder.verdict as Verdict`, above). Mirrors `seal.ts`'s own
184
+ * `validateAssembledBrief` -- same `safeParse`-then-`TypeError` shape, same
185
+ * first-issue diagnosis -- so a verdict that reached this function `null`
186
+ * despite the caller's own precondition fails loudly here instead of
187
+ * shipping a schema-invalid artifact silently.
188
+ */
189
+ function validateAssembledArtifact(artifact) {
190
+ const result = EvidenceArtifact.safeParse(artifact);
191
+ if (result.success)
192
+ return result.data;
193
+ const issueCount = result.error.issues.length;
194
+ const firstIssue = result.error.issues[0];
195
+ const firstPath = firstIssue ? dotPath(firstIssue.path) : '';
196
+ throw new TypeError(`emit(): assembled an artifact that failed EvidenceArtifact validation: ${issueCount} issue${issueCount === 1 ? '' : 's'}, first at "${firstPath === '' ? '(root)' : firstPath}"`, { cause: result.error });
197
+ }
198
+ /** `seal.ts`'s own formatter, duplicated rather than shared: both are small, stage-local, and neither exports it today. */
199
+ function dotPath(path) {
200
+ const segments = [];
201
+ for (const segment of path) {
202
+ if (typeof segment === 'number')
203
+ segments.push(`[${segment}]`);
204
+ else if (typeof segment === 'symbol' || /[^\w$]/.test(segment)) {
205
+ segments.push(`[${JSON.stringify(String(segment))}]`);
206
+ }
207
+ else {
208
+ if (segments.length > 0)
209
+ segments.push('.');
210
+ segments.push(segment);
211
+ }
212
+ }
213
+ return segments.join('');
214
+ }
@@ -0,0 +1,11 @@
1
+ import type { PrivateArtifactManifest } from '../schemas/private-artifact-manifest.ts';
2
+ /**
3
+ * Throws `RuntimeFault('digest-mismatch', ...)` on the first
4
+ * `entries[i]` whose declared `digest` disagrees with
5
+ * `resolvedDigests.get(entry.privateRef)`. An entry with no resolved digest
6
+ * at all -- `resolvedDigests` carries no key for its `privateRef` -- is the
7
+ * same disagreement: there is no byte the manifest's declared digest can be
8
+ * checked against, which this comparator treats no differently from a
9
+ * genuine mismatch.
10
+ */
11
+ export declare function checkPrivateArtifactManifestDigests(manifest: PrivateArtifactManifest, resolvedDigests: ReadonlyMap<string, string>): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The pure half of `private-artifact-manifest.ts`'s own "a mismatch is an
3
+ * AD-28 `digest-mismatch` fault": given a manifest and each entry's already-
4
+ * resolved digest, throws on the first disagreement. AD-8 requires the
5
+ * digest recomputed from the resolved bytes; resolving those bytes needs
6
+ * `CorpusPort.resolve`, an async port method, and AD-34 makes awaiting a
7
+ * port `application/`'s job, never `core/`'s. This module is the comparator
8
+ * alone and has no caller yet: whoever first awaits `CorpusPort.resolve` to
9
+ * build `resolvedDigests` is this function's caller, and that await belongs
10
+ * in `application/`, not here.
11
+ */
12
+ import { RuntimeFault } from '../schemas/faults.js';
13
+ /**
14
+ * Throws `RuntimeFault('digest-mismatch', ...)` on the first
15
+ * `entries[i]` whose declared `digest` disagrees with
16
+ * `resolvedDigests.get(entry.privateRef)`. An entry with no resolved digest
17
+ * at all -- `resolvedDigests` carries no key for its `privateRef` -- is the
18
+ * same disagreement: there is no byte the manifest's declared digest can be
19
+ * checked against, which this comparator treats no differently from a
20
+ * genuine mismatch.
21
+ */
22
+ export function checkPrivateArtifactManifestDigests(manifest, resolvedDigests) {
23
+ manifest.entries.forEach((entry, index) => {
24
+ const resolved = resolvedDigests.get(entry.privateRef);
25
+ if (resolved === entry.digest)
26
+ return;
27
+ throw new RuntimeFault('digest-mismatch', `PrivateArtifactManifest.entries[${index}]`, `entry privateRef "${entry.privateRef}" declares digest "${entry.digest}", but the resolved bytes digest to ${resolved === undefined
28
+ ? 'nothing (no resolved digest supplied for this privateRef)'
29
+ : `"${resolved}"`}`);
30
+ });
31
+ }
@@ -8,7 +8,7 @@
8
8
  import type { EvalContract } from '../schemas/eval-contract.ts';
9
9
  import type { JsonValue } from '../schemas/primitives.ts';
10
10
  import type { Observation } from '../schemas/sealed-run-record.ts';
11
- import { type PlanIndex } from '../seal/plan-index.ts';
11
+ import { type EvidenceTarget, type PlanIndex } from '../seal/plan-index.ts';
12
12
  import type { PointerDenotesCollection, ResolveOperand } from './resolution.ts';
13
13
  import { type ResolvedValue } from './resolved-value.ts';
14
14
  /**
@@ -34,6 +34,16 @@ export declare function walkTail(root: JsonValue, tail: readonly string[]): Reso
34
34
  * calling it.
35
35
  */
36
36
  export declare function decodeBoundElementTail(pointer: string): readonly string[];
37
+ /**
38
+ * Selects the channel `target` names off one `Observation`. `stdout`/`stderr`
39
+ * can carry a tail even though they're bare strings; `walkTail` already
40
+ * resolves any non-empty tail against a string to `ABSENT`, so no special
41
+ * case is needed here.
42
+ *
43
+ * Exported so `core/score/bindings.ts` reads a captured pointer's channel off
44
+ * an observation through this one spelling, keeping the switch in one place.
45
+ */
46
+ export declare function channelRoot(observation: Observation, target: EvidenceTarget): JsonValue;
37
47
  /**
38
48
  * The `ResolveOperand`. `stepObservations` holds one already-selected
39
49
  * `Observation` per interaction step; `referenceSets` mirrors the contract's
@@ -57,8 +57,11 @@ export function decodeBoundElementTail(pointer) {
57
57
  * can carry a tail even though they're bare strings; `walkTail` already
58
58
  * resolves any non-empty tail against a string to `ABSENT`, so no special
59
59
  * case is needed here.
60
+ *
61
+ * Exported so `core/score/bindings.ts` reads a captured pointer's channel off
62
+ * an observation through this one spelling, keeping the switch in one place.
60
63
  */
61
- function channelRoot(observation, target) {
64
+ export function channelRoot(observation, target) {
62
65
  switch (target.channel) {
63
66
  case 'response-body':
64
67
  return observation.responseBody;
@@ -1,5 +1,5 @@
1
- /** AD-5's twenty-one compile-time failure codes, as data, in the table's order. */
2
- export declare const FAILURE_CODES: readonly ['missing-requirement-linkage', 'no-observable-success-criterion', 'unreachable-check-evidence', 'malformed-operator-expression', 'quantifier-over-non-collection', 'quantifier-nesting-exceeded', 'unresolved-reference-set', 'duplicate-operation-signature', 'undeclared-mandatory-input', 'oracle-missing-channel', 'direction-check-misaligned', 'unsupported-interface-kind', 'nested-temporal-clause', 'plan-exceeds-scripting-bound', 'rubric-scores-reasoning-prose', 'rubric-unanchored', 'rubric-evidence-unreachable', 'forbidden-input-floor-incomplete', 'scoped-reference-resolves-forbidden', 'waiver-incomplete', 'brief-exceeds-scripting-bound'];
1
+ /** AD-5's twenty-three compile-time failure codes, as data, in the table's order. */
2
+ export declare const FAILURE_CODES: readonly ['missing-requirement-linkage', 'no-observable-success-criterion', 'unreachable-check-evidence', 'malformed-operator-expression', 'quantifier-over-non-collection', 'quantifier-nesting-exceeded', 'unresolved-reference-set', 'duplicate-operation-signature', 'undeclared-mandatory-input', 'oracle-missing-channel', 'direction-check-misaligned', 'unsupported-interface-kind', 'nested-temporal-clause', 'plan-exceeds-scripting-bound', 'binding-cycle', 'captured-channel-undeclared', 'rubric-scores-reasoning-prose', 'rubric-unanchored', 'rubric-evidence-unreachable', 'forbidden-input-floor-incomplete', 'scoped-reference-resolves-forbidden', 'waiver-incomplete', 'brief-exceeds-scripting-bound'];
3
3
  export type FailureCode = (typeof FAILURE_CODES)[number];
4
4
  /**
5
5
  * AD-5's compile-time failure registry: thrown when a structural error fails
@@ -1,11 +1,11 @@
1
- /** AD-5's twenty-one compile-time failure codes, as data, in the table's order. */
1
+ /** AD-5's twenty-three compile-time failure codes, as data, in the table's order. */
2
2
  // This lives in `core/`, since it's a plain data tuple and `core/schemas/`
3
3
  // is reserved for Zod definitions. The compiler that emits these codes lives
4
4
  // in `core/compile/`, one directory over.
5
5
  //
6
6
  // `scripts/check-ad5-registry.ts` checks this tuple against AD-5's own
7
7
  // registry table under `npm run validate`; the tuple's own invariants
8
- // (twenty-one members, unique, kebab-case) are locked in
8
+ // (twenty-three members, unique, kebab-case) are locked in
9
9
  // tests/schemas/failure-codes.test.ts.
10
10
  export const FAILURE_CODES = [
11
11
  'missing-requirement-linkage',
@@ -22,6 +22,8 @@ export const FAILURE_CODES = [
22
22
  'unsupported-interface-kind',
23
23
  'nested-temporal-clause',
24
24
  'plan-exceeds-scripting-bound',
25
+ 'binding-cycle',
26
+ 'captured-channel-undeclared',
25
27
  'rubric-scores-reasoning-prose',
26
28
  'rubric-unanchored',
27
29
  'rubric-evidence-unreachable',