eval-quality 0.2.0 → 0.5.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 (137) hide show
  1. package/README.md +17 -12
  2. package/corpus/dev/README.md +3 -2
  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/fragment-selection.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  19. package/corpus/dev/contracts/review-corpus.json +1 -0
  20. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  21. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  22. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  23. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  24. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  25. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  26. package/corpus/dev/index.json +1 -1
  27. package/dist/adapters/command-line-adapter.d.ts +44 -0
  28. package/dist/adapters/command-line-adapter.js +284 -0
  29. package/dist/adapters/command-target-policy.d.ts +36 -0
  30. package/dist/adapters/command-target-policy.js +33 -0
  31. package/dist/adapters/index.d.ts +2 -0
  32. package/dist/adapters/index.js +1 -0
  33. package/dist/cli/render.d.ts +2 -5
  34. package/dist/cli/render.js +51 -1
  35. package/dist/core/compile/bindings.d.ts +26 -7
  36. package/dist/core/compile/bindings.js +49 -32
  37. package/dist/core/compile/compile.d.ts +12 -0
  38. package/dist/core/compile/compile.js +28 -1
  39. package/dist/core/compile/excluded-content.d.ts +11 -0
  40. package/dist/core/compile/excluded-content.js +42 -0
  41. package/dist/core/compile/expression-legality.d.ts +2 -2
  42. package/dist/core/compile/expression-legality.js +58 -12
  43. package/dist/core/compile/interface-inventory.d.ts +34 -1
  44. package/dist/core/compile/interface-inventory.js +88 -11
  45. package/dist/core/compile/reachability.d.ts +12 -2
  46. package/dist/core/compile/reachability.js +123 -36
  47. package/dist/core/compile/schema-version.d.ts +2 -0
  48. package/dist/core/compile/schema-version.js +25 -0
  49. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  50. package/dist/core/compile/sensitivity-witness.js +110 -23
  51. package/dist/core/compile/step-reference.d.ts +2 -0
  52. package/dist/core/compile/step-reference.js +49 -0
  53. package/dist/core/coverage/operations.d.ts +62 -0
  54. package/dist/core/coverage/operations.js +57 -0
  55. package/dist/core/coverage/relevance.d.ts +4 -2
  56. package/dist/core/coverage/relevance.js +22 -23
  57. package/dist/core/coverage/satisfaction.d.ts +2 -2
  58. package/dist/core/coverage/satisfaction.js +73 -48
  59. package/dist/core/declared-inputs.d.ts +83 -4
  60. package/dist/core/declared-inputs.js +105 -8
  61. package/dist/core/evaluate/evidence-resolution.d.ts +9 -12
  62. package/dist/core/evaluate/evidence-resolution.js +70 -10
  63. package/dist/core/evaluate/operators.d.ts +22 -0
  64. package/dist/core/evaluate/operators.js +17 -1
  65. package/dist/core/evaluate/resolution.d.ts +14 -3
  66. package/dist/core/evaluate/resolution.js +63 -3
  67. package/dist/core/excluded-content.d.ts +65 -0
  68. package/dist/core/excluded-content.js +113 -0
  69. package/dist/core/failure-codes.d.ts +2 -2
  70. package/dist/core/failure-codes.js +5 -2
  71. package/dist/core/ingest/conditions.d.ts +1 -1
  72. package/dist/core/ingest/ingest.js +8 -0
  73. package/dist/core/preflight/plan.d.ts +7 -5
  74. package/dist/core/preflight/plan.js +75 -32
  75. package/dist/core/preflight/projection.d.ts +6 -3
  76. package/dist/core/preflight/projection.js +22 -2
  77. package/dist/core/preflight/reduce.js +47 -7
  78. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  79. package/dist/core/preflight/witness-evidence.js +63 -20
  80. package/dist/core/schemas/artifact.d.ts +683 -35
  81. package/dist/core/schemas/constraint-ledger.js +11 -0
  82. package/dist/core/schemas/defect-signature.d.ts +438 -16
  83. package/dist/core/schemas/defect-signature.js +58 -10
  84. package/dist/core/schemas/eval-contract.d.ts +517 -12
  85. package/dist/core/schemas/eval-contract.js +18 -2
  86. package/dist/core/schemas/evidence-artifact.d.ts +14 -4
  87. package/dist/core/schemas/evidence-artifact.js +1 -1
  88. package/dist/core/schemas/interface.d.ts +741 -13
  89. package/dist/core/schemas/interface.js +120 -7
  90. package/dist/core/schemas/isolation-manifest.js +16 -9
  91. package/dist/core/schemas/plan.d.ts +217 -3
  92. package/dist/core/schemas/plan.js +22 -1
  93. package/dist/core/schemas/pointer.d.ts +47 -1
  94. package/dist/core/schemas/pointer.js +89 -8
  95. package/dist/core/schemas/port-messages.d.ts +194 -10
  96. package/dist/core/schemas/port-messages.js +73 -6
  97. package/dist/core/schemas/probe-body.d.ts +18 -0
  98. package/dist/core/schemas/probe-body.js +13 -0
  99. package/dist/core/schemas/probe-policy.d.ts +36 -0
  100. package/dist/core/schemas/probe-policy.js +44 -0
  101. package/dist/core/schemas/probe.d.ts +122 -3
  102. package/dist/core/schemas/probe.js +1 -1
  103. package/dist/core/schemas/sealed-run-record.d.ts +131 -9
  104. package/dist/core/schemas/sealed-run-record.js +56 -11
  105. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  106. package/dist/core/schemas/sensitivity-witness.js +61 -5
  107. package/dist/core/score/bindings.d.ts +2 -2
  108. package/dist/core/score/bindings.js +25 -13
  109. package/dist/core/score/qualification.d.ts +5 -5
  110. package/dist/core/score/qualification.js +67 -37
  111. package/dist/core/score/quotation.d.ts +1 -24
  112. package/dist/core/score/quotation.js +29 -4
  113. package/dist/core/score/score.js +28 -2
  114. package/dist/core/score/strength.d.ts +9 -0
  115. package/dist/core/score/strength.js +35 -1
  116. package/dist/core/score/witness.d.ts +21 -0
  117. package/dist/core/score/witness.js +9 -4
  118. package/dist/core/seal/derived-reference.d.ts +2 -9
  119. package/dist/core/seal/derived-reference.js +62 -23
  120. package/dist/core/seal/plan-index.d.ts +18 -3
  121. package/dist/core/seal/plan-index.js +44 -8
  122. package/dist/index.d.ts +1 -1
  123. package/dist/index.js +1 -1
  124. package/dist/ports/environment-probe-port.d.ts +60 -4
  125. package/dist/testing/conformance.d.ts +2 -1
  126. package/dist/testing/conformance.js +1 -0
  127. package/dist/testing/index.d.ts +3 -3
  128. package/dist/testing/index.js +1 -1
  129. package/dist/testing/probe-conformance.d.ts +56 -5
  130. package/dist/testing/probe-conformance.js +196 -5
  131. package/package.json +1 -1
  132. package/schemas/eval-contract.schema.json +1356 -464
  133. package/schemas/evidence-artifact.schema.json +59 -29
  134. package/schemas/isolation-manifest.schema.json +17 -10
  135. package/schemas/probe.schema.json +197 -71
  136. package/schemas/rubric.schema.json +1 -1
  137. package/schemas/sealed-run-record.schema.json +296 -39
@@ -43,12 +43,37 @@ import { serialize } from '../canonical/canonicalize.js';
43
43
  * Should canonicalization fault on a caller's record, the fault propagates,
44
44
  * which is what keeps this procedure from being circular with it.
45
45
  */
46
- export function projectChannel(observation, channel, artifactPath) {
46
+ /**
47
+ * One tagged observed value as the text a quotation is compared against. A
48
+ * text body is its own value; a JSON body is its canonical serialisation, the
49
+ * same one `response-body` gets; an absent channel has no text.
50
+ */
51
+ function observedText(body, artifactPath) {
52
+ if (body.kind === 'absent')
53
+ return null;
54
+ if (body.kind === 'text')
55
+ return body.value;
56
+ return serialize(body.value, artifactPath);
57
+ }
58
+ export function projectChannel(observation, channel, artifactPath, artifactId = null) {
47
59
  switch (channel) {
48
60
  case 'stdout':
49
- return observation.stdout;
61
+ return observedText(observation.stdout, artifactPath);
50
62
  case 'stderr':
51
- return observation.stderr;
63
+ return observedText(observation.stderr, artifactPath);
64
+ case 'artifact': {
65
+ // One named file's own text. Serializing the whole map instead was
66
+ // wrong twice over: it escaped every newline and quotation mark, so
67
+ // a quotation from a file with more than one line could never match,
68
+ // and it searched every file at once, so a quotation from a file the
69
+ // finding did not cite matched anyway.
70
+ if (artifactId === null)
71
+ return null;
72
+ if (!Object.hasOwn(observation.artifacts, artifactId))
73
+ return null;
74
+ const written = observation.artifacts[artifactId];
75
+ return written === undefined ? null : observedText(written, artifactPath);
76
+ }
52
77
  case 'response-status':
53
78
  return observation.responseStatus === null
54
79
  ? null
@@ -73,7 +98,7 @@ export function projectChannel(observation, channel, artifactPath) {
73
98
  * Whether one quotation appears in one observation's named channel, verbatim.
74
99
  */
75
100
  function quotationWitnessed(quoted, observation, artifactPath) {
76
- const projected = projectChannel(observation, quoted.channel, artifactPath);
101
+ const projected = projectChannel(observation, quoted.channel, artifactPath, quoted.artifactId);
77
102
  // A null projection witnesses nothing, and `false` is the answer: an
78
103
  // optional chain here would hand a caller `undefined` where the return type
79
104
  // says boolean.
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import { walkExpression } from '../compile/expression-legality.js';
32
32
  import { evaluateCoverage } from '../coverage/coverage.js';
33
- import { makePointerDenotesCollection, makeResolveOperand, } from '../evaluate/evidence-resolution.js';
33
+ import { makePointerDenotesCollection, makeResolveOperand, referenceSetKeysOf, } from '../evaluate/evidence-resolution.js';
34
34
  import { resolveCheck } from '../evaluate/resolution.js';
35
35
  import { SEVERITY_LEVELS, } from '../schemas/eval-contract.js';
36
36
  import { buildPlanIndex } from '../seal/plan-index.js';
@@ -195,6 +195,29 @@ function trialSetDisagreementsOf(trials) {
195
195
  });
196
196
  return disagreements;
197
197
  }
198
+ /**
199
+ * Cross-trial identifier reuse is NOT a collision, and this is where that was
200
+ * settled after being carried as an open question.
201
+ *
202
+ * `ingest` computes `duplicate-record-identifier` per sealed run record, since
203
+ * it sees one trial at a time, and nothing checks whether two DIFFERENT trials
204
+ * of one set reuse an observation, finding, or oracle-disposition identifier.
205
+ * That asymmetry looks like a gap beside `trial-set-field-disagreement`, which
206
+ * does compare `mode` and `evaluatorRecommendation` across trials.
207
+ *
208
+ * It is not one. A trial set is n independent evaluator runs of one contract,
209
+ * each producing its own record, and a harness that names its first observation
210
+ * `obs-1` names it that in every run. Reporting the second run for it would
211
+ * make a repeated run Invalid by construction, which is the opposite of what a
212
+ * trial set is for. The fields that ARE compared across trials are compared
213
+ * because they describe the SET rather than a run within it: two trials
214
+ * disagreeing about which mode they ran under cannot both be true of one set,
215
+ * while two trials both carrying `obs-1` are two true statements about two
216
+ * runs.
217
+ *
218
+ * The pooled basis this stage builds carries each entry with the trial it came
219
+ * from, so a repeated identifier is never resolved against the wrong row.
220
+ */
198
221
  /** Every `IngestCondition` of one `kind` across every trial, in trial order. */
199
222
  function conditionsAcrossTrials(trials, kind) {
200
223
  return trials.flatMap((trial) => trial.conditions.filter((condition) => condition.kind === kind));
@@ -246,6 +269,9 @@ export const score = (contract, trials, probe, preflightVerdict, policy, waiver,
246
269
  id,
247
270
  declaration.members,
248
271
  ]));
272
+ // The declared keys the members map above drops. `set-membership`'s set
273
+ // position reads the single declared key off each member.
274
+ const referenceSetKeys = referenceSetKeysOf(contract);
249
275
  const judgeConduct = judgeConductOf(contract, trials);
250
276
  const allOutcomes = [];
251
277
  // The full `EvidenceArtifact.outcomes` shape, built alongside `allOutcomes`
@@ -317,7 +343,7 @@ export const score = (contract, trials, probe, preflightVerdict, policy, waiver,
317
343
  entry.step.cardinality !== 'any');
318
344
  const checkResolution = check === null
319
345
  ? null
320
- : resolveCheck(check, resolveOperand, pointerDenotesCollection, policy.regexMatchStepBudget, `EvalContract.oracles[id=${oracle.id}].check`);
346
+ : resolveCheck(check, resolveOperand, pointerDenotesCollection, referenceSetKeys, policy.regexMatchStepBudget, `EvalContract.oracles[id=${oracle.id}].check`);
321
347
  // Every disposition this trial records for this oracle, not just
322
348
  // the first: `SealedRunRecord.oracleDispositions` carries no
323
349
  // uniqueness constraint on `oracleId`, and ingest's
@@ -45,5 +45,14 @@ export declare function buildStrengthVector(admitted: readonly QualifiedProbe[],
45
45
  * on it is not fit to decide a comparison either way. The severity-floor
46
46
  * override runs only against the side the raw comparison favoured, and only
47
47
  * ever downgrades that result to `incomparable`.
48
+ *
49
+ * That scope is AD-7's own and not an omission: its words are that a contract
50
+ * missing a behaviour at or above the floor "never dominates" one that caught
51
+ * it, which constrains dominance and says nothing about equivalence. Two
52
+ * vectors that are component-wise equal are `equivalent` whatever their
53
+ * severities, because neither is dominating anything for the override to
54
+ * withdraw. Widening it to `equivalent` would be a new rule rather than this
55
+ * one applied more thoroughly, and it is written down here so the asymmetry
56
+ * reads as a decision rather than a gap.
48
57
  */
49
58
  export declare function compareDominance(a: ComparableResult, b: ComparableResult, severityFloor: Severity): DominanceRelationValue;
@@ -44,7 +44,18 @@ const classStrengthOf = (probesInClass, results) => {
44
44
  return null;
45
45
  let exercised = 0;
46
46
  let caught = 0;
47
+ // Counted once per identifier, not once per entry. AD-7's rate is over
48
+ // unique qualified probe identifiers, and nothing in `src/` enforces that
49
+ // `admitted` carries each identifier once: no probe-corpus schema exists,
50
+ // so the uniqueness is inherited from upstream qualification rather than
51
+ // checked. A repeated identifier would otherwise count its trial-set
52
+ // result twice on both sides of the same ratio, which leaves the rate
53
+ // right and the raw counts wrong.
54
+ const counted = new Set();
47
55
  for (const { probe } of probesInClass) {
56
+ if (counted.has(probe.probeId))
57
+ continue;
58
+ counted.add(probe.probeId);
48
59
  const result = results.get(probe.probeId);
49
60
  if (result === undefined || !result.exercised)
50
61
  continue;
@@ -123,7 +134,21 @@ function componentComparison(a, b) {
123
134
  return 'b-dominates-a';
124
135
  return 'incomparable';
125
136
  }
126
- const atOrAboveFloor = (severity, floor) => SEVERITY_LEVELS.indexOf(severity) >= SEVERITY_LEVELS.indexOf(floor);
137
+ /**
138
+ * Both operands are looked up before they are compared, because `indexOf`
139
+ * answers `-1` for a value the ladder does not name and `-1 >= -1` reads as
140
+ * "at or above the floor" for two values that are on no ladder at all. A
141
+ * severity outside the closed set is not at or above anything, and a floor
142
+ * outside it bounds nothing, so either one absent is `false` rather than a
143
+ * comparison of two absences.
144
+ */
145
+ const atOrAboveFloor = (severity, floor) => {
146
+ const rank = SEVERITY_LEVELS.indexOf(severity);
147
+ const bound = SEVERITY_LEVELS.indexOf(floor);
148
+ if (rank < 0 || bound < 0)
149
+ return false;
150
+ return rank >= bound;
151
+ };
127
152
  /**
128
153
  * Keyed by the first outcome carrying each `probeId`, not the last: two
129
154
  * `Outcome` entries sharing one `probeId` is itself a defect somewhere
@@ -174,6 +199,15 @@ function favoredMissesWhatOtherCaught(favored, other, severityFloor) {
174
199
  * on it is not fit to decide a comparison either way. The severity-floor
175
200
  * override runs only against the side the raw comparison favoured, and only
176
201
  * ever downgrades that result to `incomparable`.
202
+ *
203
+ * That scope is AD-7's own and not an omission: its words are that a contract
204
+ * missing a behaviour at or above the floor "never dominates" one that caught
205
+ * it, which constrains dominance and says nothing about equivalence. Two
206
+ * vectors that are component-wise equal are `equivalent` whatever their
207
+ * severities, because neither is dominating anything for the override to
208
+ * withdraw. Widening it to `equivalent` would be a new rule rather than this
209
+ * one applied more thoroughly, and it is written down here so the asymmetry
210
+ * reads as a decision rather than a gap.
177
211
  */
178
212
  export function compareDominance(a, b, severityFloor) {
179
213
  if (a.comparabilityKey !== b.comparabilityKey)
@@ -1,3 +1,24 @@
1
+ /**
2
+ * AD-40's witness match: whether a finding cited against a probe actually
3
+ * witnessed the defect that probe seeded.
4
+ *
5
+ * This is the input that makes non-detection reachable. Without it, an oracle
6
+ * that correctly confirmed an untouched behaviour and one that failed to detect
7
+ * the seeded defect present identical inputs, so any table obeying AD-3 must
8
+ * give both the same answer and the catch rate is 1.00 by construction.
9
+ *
10
+ * Pure and total on its ordinary path, and no AD-6 outcome state is assigned
11
+ * here. The result is a fact about evidence; turning it into an outcome state
12
+ * is the decision procedure's.
13
+ *
14
+ * One deliberate non-totality: a `RuntimeFault` from the shipped evaluator
15
+ * propagates undecorated. A fault never becomes a verdict, and AD-28 makes it
16
+ * an invalidating condition under AD-21 rather than a behavioural result, so
17
+ * catching one here would convert AD-21's fault exit into a scored run. The
18
+ * qualification gate rejects every operand class that makes the evaluator's
19
+ * four plain-`Error` sites reachable, which leaves exactly two data-dependent
20
+ * `RuntimeFault`s: an exhausted regex budget and a non-canonicalizable value.
21
+ */
1
22
  import type { DefectSignature } from '../schemas/defect-signature.ts';
2
23
  import type { PermittedInterface } from '../schemas/interface.ts';
3
24
  import type { Probe } from '../schemas/probe.ts';
@@ -19,11 +19,12 @@
19
19
  * four plain-`Error` sites reachable, which leaves exactly two data-dependent
20
20
  * `RuntimeFault`s: an exhausted regex budget and a non-canonicalizable value.
21
21
  */
22
+ import { requestShapeOf } from '../declared-inputs.js';
22
23
  import { makeResolveOperand } from '../evaluate/evidence-resolution.js';
23
24
  import { resolveCheck } from '../evaluate/resolution.js';
24
25
  import { makeWitnessPointerDenotesCollection } from '../preflight/witness-evidence.js';
25
26
  import { OBSERVED_STEP_ID } from '../schemas/defect-signature.js';
26
- import { TRANSPORT_CHANNELS } from '../schemas/pointer.js';
27
+ import { INPUT_CHANNELS } from '../schemas/pointer.js';
27
28
  import { deepEquals, jsonTypeOf } from './bindings.js';
28
29
  import { resolveHomeOperation } from './qualification.js';
29
30
  /**
@@ -73,7 +74,7 @@ export const PROBE_WITNESS_RESULTS = [
73
74
  * probe has no interaction plan to index.
74
75
  */
75
76
  function selectorAdmits(binding, observation, operation) {
76
- for (const channel of TRANSPORT_CHANNELS) {
77
+ for (const channel of INPUT_CHANNELS) {
77
78
  const channelBinding = binding[channel];
78
79
  if (channelBinding === null)
79
80
  continue;
@@ -94,7 +95,7 @@ function selectorAdmits(binding, observation, operation) {
94
95
  }
95
96
  if (value.matcher === 'any')
96
97
  continue;
97
- const declared = operation.requestShape[channel].types[key];
98
+ const declared = requestShapeOf(operation, channel)?.types[key];
98
99
  if (declared === undefined || declared === null)
99
100
  return false;
100
101
  if (jsonTypeOf(actual) === declared)
@@ -119,7 +120,11 @@ const bySequence = (a, b) => a.sequence - b.sequence || (a.observationId < b.obs
119
120
  * than an accident.
120
121
  */
121
122
  function resolveCondition(signature, observation, operation, artifactPath) {
122
- return resolveCheck(signature.condition.predicate, makeResolveOperand({ [OBSERVED_STEP_ID]: observation }, {}), makeWitnessPointerDenotesCollection(operation), PROBE_REGEX_MATCH_STEP_BUDGET, artifactPath).resolution;
123
+ return resolveCheck(signature.condition.predicate, makeResolveOperand({ [OBSERVED_STEP_ID]: observation }, {}), makeWitnessPointerDenotesCollection(operation),
124
+ // No reference sets, matching the empty members map above: the
125
+ // qualification gate rejects a reference-set operand in a defect
126
+ // signature under `condition-reference-set-operand`.
127
+ {}, PROBE_REGEX_MATCH_STEP_BUDGET, artifactPath).resolution;
123
128
  }
124
129
  const defectFindingsFor = (findings, probeId) => findings.filter((finding) => finding.findingType === 'defect' && finding.probeId === probeId);
125
130
  /**
@@ -1,11 +1,4 @@
1
- /**
2
- * The derived-reference vocabulary (AD-16). Renders an evidence-target
3
- * pointer, or a temporally-paired pair, as a description of the step's
4
- * operation and selection predicate; the step's own identifier never appears.
5
- * Resolves pointers through a `PlanIndex` into a phrase, kept apart from
6
- * `plan-index.ts`'s resolving and `direction-prose.ts`'s relation templates.
7
- */
8
- import type { Operation } from '../schemas/interface.ts';
1
+ import type { AnyOperation } from '../schemas/interface.ts';
9
2
  import type { InteractionStep } from '../schemas/plan.ts';
10
3
  import { type PlanIndex } from './plan-index.ts';
11
4
  /**
@@ -21,7 +14,7 @@ import { type PlanIndex } from './plan-index.ts';
21
14
  * duplicate `stepId` check if two siblings still collide after full
22
15
  * escalation.
23
16
  */
24
- export declare function renderStepReference(step: InteractionStep, operation: Operation, siblings: readonly InteractionStep[], index: PlanIndex): string;
17
+ export declare function renderStepReference(step: InteractionStep, operation: AnyOperation, siblings: readonly InteractionStep[], index: PlanIndex): string;
25
18
  /**
26
19
  * Renders every evidence target a direction declares as one canonically
27
20
  * ordered, order-independent clause: byte-identical under any permutation of
@@ -1,8 +1,20 @@
1
- import { parseEvidenceTarget, resolveOperation, resolveStep, } from './plan-index.js';
1
+ /**
2
+ * The derived-reference vocabulary (AD-16). Renders an evidence-target
3
+ * pointer, or a temporally-paired pair, as a description of the step's
4
+ * operation and selection predicate; the step's own identifier never appears.
5
+ * Resolves pointers through a `PlanIndex` into a phrase, kept apart from
6
+ * `plan-index.ts`'s resolving and `direction-prose.ts`'s relation templates.
7
+ */
8
+ import { boundChannelsOf, isCommandOperation } from '../declared-inputs.js';
9
+ import { StructuralFailure } from '../failure-codes.js';
10
+ import { anyOperationOf, parseEvidenceTarget, resolveStep, } from './plan-index.js';
2
11
  function resolveEvidenceTarget(pointer, index) {
3
12
  const target = parseEvidenceTarget(pointer);
4
13
  const step = resolveStep(index, target.stepId);
5
- const operation = resolveOperation(index, step.operationId);
14
+ const operation = anyOperationOf(index, step.operationId);
15
+ if (operation === undefined) {
16
+ throw new TypeError(`step names an operation the permitted interfaces do not declare: ${step.operationId}`);
17
+ }
6
18
  return { target, step, operation };
7
19
  }
8
20
  // ---- joining ---------------------------------------------------------
@@ -21,30 +33,31 @@ function joinWithAnd(items) {
21
33
  // ---- operation identity -----------------------------------------------
22
34
  // Humanizing a kebab-case operationId is injective on distinct ids, and
23
35
  // `buildPlanIndex` already rejects a duplicate `operationId` across
24
- // interfaces, so two resolved operations never share this phrase. `method`
25
- // and `pathTemplate` are never printed here: AD-16 withholds the operation
26
- // inventory from the brief.
36
+ // interfaces, so two resolved operations never share this phrase. The
37
+ // transport identity is never printed here, whichever kind it is: AD-16
38
+ // withholds the operation inventory from the brief, so neither a method and a
39
+ // path template nor an executable and a subcommand path reach an evaluator.
40
+ //
41
+ // The noun follows the kind. Calling a command an endpoint told the evaluator
42
+ // something false about what it was reading, and the word is the only thing
43
+ // this phrase says beyond the operation's own name.
27
44
  function operationReference(operation) {
28
- return `the ${operation.operationId.split('-').join(' ')} endpoint`;
45
+ const name = operation.operationId.split('-').join(' ');
46
+ return isCommandOperation(operation)
47
+ ? `the ${name} command`
48
+ : `the ${name} endpoint`;
29
49
  }
30
50
  const ESCALATION_LEVELS = [
31
51
  'generic',
32
52
  'kind',
33
53
  'literal',
34
54
  ];
35
- const TRANSPORT_ORDER = [
36
- 'path',
37
- 'query',
38
- 'header',
39
- 'body',
40
- ];
41
- // Sorted by transport channel in fixed order, then by key name, so this
55
+ // Sorted by input channel in `INPUT_CHANNELS` order, then by key name, so this
42
56
  // never depends on a binding map's insertion order and the rendered prose
43
- // stays permutation-invariant.
57
+ // stays permutation-invariant. `boundChannelsOf` supplies the channel order.
44
58
  function bindingEntries(step) {
45
59
  const entries = [];
46
- for (const transportChannel of TRANSPORT_ORDER) {
47
- const map = step.inputBinding[transportChannel];
60
+ for (const { channel: transportChannel, bound: map } of boundChannelsOf(step.inputBinding)) {
48
61
  if (map === null)
49
62
  continue;
50
63
  for (const key of Object.keys(map).sort()) {
@@ -99,9 +112,10 @@ function formatLiteral(literal) {
99
112
  // predecessor separates them by its own binding clause. Two predecessors that
100
113
  // are themselves irreducible still tie, and the throw there is correct: the
101
114
  // declared structure does not distinguish them, which is the same answer
102
- // `irreducibleCollisionPair` already gets. That throw is a bare `TypeError`
103
- // rather than a coded failure, which is a pre-existing gap this widens; the
104
- // deferred-work entry names hoisting the collision check to compile time.
115
+ // `irreducibleCollisionPair` already gets. That throw is a coded
116
+ // `StructuralFailure` under `irreducible-step-reference`, and it is
117
+ // hoisted to compile time by `checkStepReferenceReducibility`, which runs this
118
+ // same ladder and reports the same code before `seal` is ever called.
105
119
  //
106
120
  // It does NOT call `renderStepReference`, so none of that function's
107
121
  // constraints apply: no direction-scoped sibling list is consulted, so AD-16's
@@ -191,7 +205,7 @@ function renderCaptureGroup(group, level, index, rendering, budget) {
191
205
  const names = joinWithAnd(group.entries.map(entryName));
192
206
  const locals = joinWithAnd(group.targets.map(localTargetPhrase));
193
207
  const step = index.stepOf(group.stepId);
194
- const operation = step === undefined ? undefined : index.operationOf(step.operationId);
208
+ const operation = step === undefined ? undefined : anyOperationOf(index, step.operationId);
195
209
  if (step === undefined || operation === undefined) {
196
210
  return unexpandedGroup(group);
197
211
  }
@@ -222,7 +236,7 @@ function expandableCapture(entry, level, index, rendering, budget) {
222
236
  const step = index.stepOf(target.stepId);
223
237
  if (step === undefined)
224
238
  return null;
225
- return index.operationOf(step.operationId) === undefined ? null : target;
239
+ return anyOperationOf(index, step.operationId) === undefined ? null : target;
226
240
  }
227
241
  // Renders one entry on its own. Every captured entry that expands lands in a
228
242
  // group instead, so the arm here is the level-independent fallback.
@@ -355,7 +369,13 @@ export function renderStepReference(step, operation, siblings, index) {
355
369
  if (!starved)
356
370
  break;
357
371
  }
358
- throw new TypeError(`two or more steps invoking operation "${operation.operationId}" that this direction references render to the same derived reference even fully escalated; the declared structure does not distinguish them`);
372
+ // Coded rather than a bare `TypeError`. A contract reaching here passed
373
+ // every compile check and then failed at seal time with a stack trace, and
374
+ // the defect is an ordinary authoring fault: two steps the declared
375
+ // structure does not tell apart. `checkStepReferenceReducibility` runs this
376
+ // same ladder at compile time so the fault is reported before seal, and
377
+ // this throw is what it catches.
378
+ throw new StructuralFailure('irreducible-step-reference', `EvalContract.interactionPlan[operationId=${operation.operationId}]`, `two or more steps invoking operation "${operation.operationId}" that one direction references render to the same derived reference even fully escalated; the declared structure does not distinguish them (AD-16, AD-3)`);
359
379
  }
360
380
  function siblingsByOperation(resolved) {
361
381
  const map = new Map();
@@ -422,6 +442,20 @@ function localTargetPhrase(target) {
422
442
  return field !== null
423
443
  ? `the ${field} field of its standard error`
424
444
  : 'the standard error you obtained';
445
+ case 'artifact': {
446
+ if (target.artifactId === null) {
447
+ // Unreachable: `parseEvidenceTarget` sets this exactly when the
448
+ // channel is 'artifact'.
449
+ throw new TypeError('artifact evidence target names no artifact');
450
+ }
451
+ // The artifact is named for the same reason the transport channel
452
+ // is: two files can carry the same field name, and an evaluator
453
+ // reading "its findings field" twice cannot tell which file it
454
+ // means.
455
+ return field !== null
456
+ ? `the ${field} field of the ${target.artifactId} it wrote`
457
+ : `the ${target.artifactId} it wrote`;
458
+ }
425
459
  }
426
460
  }
427
461
  function fullTargetPhrase(resolved, siblingsOf, index) {
@@ -436,7 +470,12 @@ function fullTargetPhrase(resolved, siblingsOf, index) {
436
470
  // 'b']`). Both would join to the same `".../a/b"` string, producing a false
437
471
  // tie in `sentFirstOrder` below.
438
472
  function channelSignature(target) {
439
- return JSON.stringify([target.channel, target.transportChannel, target.tail]);
473
+ return JSON.stringify([
474
+ target.channel,
475
+ target.transportChannel,
476
+ target.artifactId,
477
+ target.tail,
478
+ ]);
440
479
  }
441
480
  // A total order over one pair, independent of argument order. The final
442
481
  // reference-comparison rung fires only on a genuine tie, such as the same
@@ -1,7 +1,7 @@
1
1
  /** resolves a pointer to its step and operation; nothing about reachability. */
2
- import type { Operation, PermittedInterface } from '../schemas/interface.ts';
2
+ import type { AnyOperation, CommandOperation, InterfaceKindName, Operation, PermittedInterface } from '../schemas/interface.ts';
3
3
  import type { InteractionStep } from '../schemas/plan.ts';
4
- import { type EvidenceChannelName, type TransportChannelName } from '../schemas/pointer.ts';
4
+ import { type EvidenceChannelName, type InputChannelName } from '../schemas/pointer.ts';
5
5
  /**
6
6
  * Exported so `core/evaluate/evidence-resolution.ts` decodes pointer tails
7
7
  * with these same two functions, avoiding a second private copy (the drift
@@ -17,7 +17,8 @@ export declare const decodeTail: (tailSource: string) => readonly string[];
17
17
  export type EvidenceTarget = {
18
18
  stepId: string;
19
19
  channel: EvidenceChannelName;
20
- transportChannel: TransportChannelName | null;
20
+ transportChannel: InputChannelName | null;
21
+ artifactId: string | null;
21
22
  tail: readonly string[];
22
23
  };
23
24
  /**
@@ -34,10 +35,18 @@ export declare function parseEvidenceTarget(pointer: string): EvidenceTarget;
34
35
  * about reachability or channel typing; the general addressing-grammar
35
36
  * resolver lives in `core/evaluate/evidence-resolution.ts` and reachability
36
37
  * in `core/compile/reachability.ts`.
38
+ *
39
+ * `operationOf` stays narrow on purpose. It hands a resolved `Operation` to
40
+ * every downstream consumer, so widening its return type to the operation
41
+ * union would retype seventeen files at once. A command operation resolves to
42
+ * `undefined` from it and to a value from `commandOperationOf`, and each
43
+ * caller that has to branch reads the declaring kind first.
37
44
  */
38
45
  export type PlanIndex = {
39
46
  stepOf: (stepId: string) => InteractionStep | undefined;
40
47
  operationOf: (operationId: string) => Operation | undefined;
48
+ commandOperationOf: (operationId: string) => CommandOperation | undefined;
49
+ interfaceKindOf: (operationId: string) => InterfaceKindName | undefined;
41
50
  stepsUsing: (operationId: string) => readonly InteractionStep[];
42
51
  };
43
52
  export type PlanIndexOptions = {
@@ -59,5 +68,11 @@ export declare function buildPlanIndex(interactionPlan: readonly InteractionStep
59
68
  * `RuntimeFault`.
60
69
  */
61
70
  export declare function resolveStep(index: PlanIndex, stepId: string): InteractionStep;
71
+ /**
72
+ * The declared operation of whichever kind, for the callers that read only
73
+ * fields both shapes carry. Callers reading a kind-specific field ask
74
+ * `interfaceKindOf` first and then take the matching accessor.
75
+ */
76
+ export declare const anyOperationOf: (index: PlanIndex, operationId: string) => AnyOperation | undefined;
62
77
  /** Resolves an operation id through the index or throws. See `resolveStep`. */
63
78
  export declare function resolveOperation(index: PlanIndex, operationId: string): Operation;
@@ -1,4 +1,5 @@
1
- import { SCALAR_CHANNELS, TAIL_BEARING_CHANNELS, TRANSPORT_CHANNELS, TRANSPORT_ROOTED_CHANNEL, } from '../schemas/pointer.js';
1
+ import { operationsOf } from '../schemas/interface.js';
2
+ import { IDENTIFIER_ROOTED_CHANNEL, INPUT_CHANNELS, SCALAR_CHANNELS, TAIL_BEARING_CHANNELS, TRANSPORT_ROOTED_CHANNEL, } from '../schemas/pointer.js';
2
3
  import { IDENTIFIER_CHARSET_SOURCE } from '../schemas/primitives.js';
3
4
  // An RFC 6901 reference token, respelled here because pointer.ts keeps its
4
5
  // own TOKEN/TAIL fragments private. IDENTIFIER_CHARSET_SOURCE and the channel
@@ -14,10 +15,10 @@ const alternation = (members) => members.join('|');
14
15
  // Named capture groups replace positional indices, avoiding a fragile
15
16
  // dependency on `IDENTIFIER_CHARSET_SOURCE` staying free of its own capturing
16
17
  // groups.
17
- const EVIDENCE_TARGET_PATTERN = new RegExp(`^/interactions/(?<stepId>${IDENTIFIER_CHARSET_SOURCE})/(?:(?<tailBearingChannel>${alternation(TAIL_BEARING_CHANNELS)})(?<tailBearingTail>${TAIL_SOURCE})|(?<scalarChannel>${alternation(SCALAR_CHANNELS)})|${TRANSPORT_ROOTED_CHANNEL}/(?<transportChannel>${alternation(TRANSPORT_CHANNELS)})(?<callInputsTail>${TAIL_SOURCE}))$`);
18
+ const EVIDENCE_TARGET_PATTERN = new RegExp(`^/interactions/(?<stepId>${IDENTIFIER_CHARSET_SOURCE})/(?:(?<tailBearingChannel>${alternation(TAIL_BEARING_CHANNELS)})(?<tailBearingTail>${TAIL_SOURCE})|(?<scalarChannel>${alternation(SCALAR_CHANNELS)})|${TRANSPORT_ROOTED_CHANNEL}/(?<transportChannel>${alternation(INPUT_CHANNELS)})(?<callInputsTail>${TAIL_SOURCE})|${IDENTIFIER_ROOTED_CHANNEL}/(?<artifactId>${IDENTIFIER_CHARSET_SOURCE})(?<artifactTail>${TAIL_SOURCE}))$`);
18
19
  const isEvidenceChannel = (value) => TAIL_BEARING_CHANNELS.includes(value) ||
19
20
  SCALAR_CHANNELS.includes(value);
20
- const isTransportChannel = (value) => TRANSPORT_CHANNELS.includes(value);
21
+ const isInputChannel = (value) => INPUT_CHANNELS.includes(value);
21
22
  /**
22
23
  * Exported so `core/evaluate/evidence-resolution.ts` decodes pointer tails
23
24
  * with these same two functions, avoiding a second private copy (the drift
@@ -47,6 +48,7 @@ export function parseEvidenceTarget(pointer) {
47
48
  stepId,
48
49
  channel: groups.scalarChannel,
49
50
  transportChannel: null,
51
+ artifactId: null,
50
52
  tail: [],
51
53
  };
52
54
  }
@@ -59,21 +61,32 @@ export function parseEvidenceTarget(pointer) {
59
61
  stepId,
60
62
  channel: groups.tailBearingChannel,
61
63
  transportChannel: null,
64
+ artifactId: null,
62
65
  tail: decodeTail(groups.tailBearingTail ?? ''),
63
66
  };
64
67
  }
65
68
  if (groups.transportChannel !== undefined) {
66
- if (!isTransportChannel(groups.transportChannel)) {
67
- // Unreachable: TRANSPORT_CHANNELS is exactly what this group can match.
69
+ if (!isInputChannel(groups.transportChannel)) {
70
+ // Unreachable: INPUT_CHANNELS is exactly what this group can match.
68
71
  throw new TypeError(`call-inputs evidence target names no transport channel: ${pointer}`);
69
72
  }
70
73
  return {
71
74
  stepId,
72
75
  channel: 'call-inputs',
73
76
  transportChannel: groups.transportChannel,
77
+ artifactId: null,
74
78
  tail: decodeTail(groups.callInputsTail ?? ''),
75
79
  };
76
80
  }
81
+ if (groups.artifactId !== undefined) {
82
+ return {
83
+ stepId,
84
+ channel: IDENTIFIER_ROOTED_CHANNEL,
85
+ transportChannel: null,
86
+ artifactId: groups.artifactId,
87
+ tail: decodeTail(groups.artifactTail ?? ''),
88
+ };
89
+ }
77
90
  // Unreachable: the pattern's three branches are exhaustive once stepId matched.
78
91
  throw new TypeError(`not an interaction-rooted evidence pointer of a recognized channel: ${pointer}`);
79
92
  }
@@ -109,25 +122,42 @@ export function buildPlanIndex(interactionPlan, permittedInterfaces, options = {
109
122
  }
110
123
  }
111
124
  const operations = new Map();
125
+ const commandOperations = new Map();
126
+ const kinds = new Map();
112
127
  const duplicateOperationIds = new Set();
113
128
  for (const iface of permittedInterfaces) {
114
- for (const operation of iface.operations) {
115
- if (operations.has(operation.operationId) ||
129
+ for (const operation of operationsOf(iface)) {
130
+ if (kinds.has(operation.operationId) ||
116
131
  duplicateOperationIds.has(operation.operationId)) {
117
132
  if (duplicateIds === 'throw') {
118
133
  throw new TypeError(`duplicate operation id across permitted interfaces: ${operation.operationId}`);
119
134
  }
120
135
  operations.delete(operation.operationId);
136
+ commandOperations.delete(operation.operationId);
137
+ kinds.delete(operation.operationId);
121
138
  duplicateOperationIds.add(operation.operationId);
122
139
  }
123
140
  else {
124
- operations.set(operation.operationId, operation);
141
+ kinds.set(operation.operationId, iface.kind);
142
+ // Sorted into the two maps by the interface's own kind rather
143
+ // than by probing the operation for a field: `web` and `mcp`
144
+ // carry the api operation shape and belong in the same map as
145
+ // `api`, since every consumer of a resolved operation reads
146
+ // the same declared fields off all three.
147
+ if (iface.kind === 'cli') {
148
+ commandOperations.set(operation.operationId, operation);
149
+ }
150
+ else {
151
+ operations.set(operation.operationId, operation);
152
+ }
125
153
  }
126
154
  }
127
155
  }
128
156
  return {
129
157
  stepOf: (stepId) => steps.get(stepId),
130
158
  operationOf: (operationId) => operations.get(operationId),
159
+ commandOperationOf: (operationId) => commandOperations.get(operationId),
160
+ interfaceKindOf: (operationId) => kinds.get(operationId),
131
161
  stepsUsing: (operationId) => stepsByOperation.get(operationId) ?? [],
132
162
  };
133
163
  }
@@ -145,6 +175,12 @@ export function resolveStep(index, stepId) {
145
175
  }
146
176
  return step;
147
177
  }
178
+ /**
179
+ * The declared operation of whichever kind, for the callers that read only
180
+ * fields both shapes carry. Callers reading a kind-specific field ask
181
+ * `interfaceKindOf` first and then take the matching accessor.
182
+ */
183
+ export const anyOperationOf = (index, operationId) => index.operationOf(operationId) ?? index.commandOperationOf(operationId);
148
184
  /** Resolves an operation id through the index or throws. See `resolveStep`. */
149
185
  export function resolveOperation(index, operationId) {
150
186
  const operation = index.operationOf(operationId);
package/dist/index.d.ts CHANGED
@@ -12,4 +12,4 @@ export type { ScoringPolicy } from './core/schemas/scoring-policy.ts';
12
12
  export type { SealedEvaluatorBrief } from './core/schemas/sealed-evaluator-brief.ts';
13
13
  export type { SealedRunRecord } from './core/schemas/sealed-run-record.ts';
14
14
  export type { FixtureReset, ManifestationWitness, SensitivityWitness, SensitivityWitnessLeg, WitnessChannel, WitnessInputs, } from './core/schemas/sensitivity-witness.ts';
15
- export declare const VERSION = "0.2.0";
15
+ export declare const VERSION = "0.5.0";
package/dist/index.js CHANGED
@@ -19,4 +19,4 @@
19
19
  // subpath, where AD-37 puts the conformance definition an adapter author
20
20
  // reads; the reference adapters stay at `eval-quality/adapters`.
21
21
  export * from './application/index.js';
22
- export const VERSION = '0.2.0';
22
+ export const VERSION = '0.5.0';