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
@@ -0,0 +1,113 @@
1
+ /**
2
+ * AD-18's excluded categories, as patterns that read a value rather than a name.
3
+ *
4
+ * AD-18 excludes "credentials, tokens, real names, email addresses, account
5
+ * identifiers, and transaction content" from every artifact this package
6
+ * produces or publishes, and binds "published examples and test fixtures as
7
+ * strictly as real runs". Until this module the whole prohibition was carried by
8
+ * one test that scans `corpus/dev`, so a contract carrying a live-shaped token
9
+ * compiled clean and the token travelled into the sealed brief.
10
+ *
11
+ * Only the value-shaped half of that scan ships here, and the split is the point
12
+ * rather than an economy. AD-18's own reading of the rule for the header channel
13
+ * is that a declaration "names the header and its type and never carries a
14
+ * credential value", so a name is what an author is supposed to write: an
15
+ * operation declaring a `password` body key or an `Authorization` header is a
16
+ * contract for an authentication API, and a gate matching `\bpassword\b` would
17
+ * reject exactly the systems this package exists to evaluate. The patterns below
18
+ * therefore match only strings whose shape is decidable without knowing what
19
+ * field they sit in: a PEM header, a token with an issuer prefix, an address, a
20
+ * checkable account number, a card-shaped digit run. The name-shaped patterns
21
+ * stay in `tests/architecture/dev-corpus.test.ts`, where review rather than a
22
+ * compiler decides.
23
+ *
24
+ * Each pattern carries a string it must fire on, and a test asserts every one
25
+ * still does. A pattern that has rotted into a regex matching nothing is worse
26
+ * than no pattern: it reports clean forever.
27
+ */
28
+ /** AD-18's six categories, in the rule's own order. */
29
+ export const EXCLUDED_CATEGORIES = [
30
+ 'credentials',
31
+ 'tokens',
32
+ 'real names',
33
+ 'email addresses',
34
+ 'account identifiers',
35
+ 'transaction content',
36
+ ];
37
+ /**
38
+ * `real names` has no member here and never will: a personal name has no surface
39
+ * form, so every pattern that reaches one reads the field that carries it, which
40
+ * is a name-shaped test. The category stays in the tuple above because AD-18
41
+ * names six and dropping one from the vocabulary would make the gate look total.
42
+ */
43
+ export const EXCLUDED_VALUE_PATTERNS = [
44
+ {
45
+ category: 'credentials',
46
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
47
+ fires: '-----BEGIN RSA PRIVATE KEY-----',
48
+ },
49
+ {
50
+ category: 'tokens',
51
+ regex: /(?<![A-Za-z])(?:[Bb]earer\s+[\w.~+/-]{16,}|eyJ[\w-]{8,}\.[\w-]{8,}|sk-[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16})/,
52
+ fires: 'Authorization: Bearer abcdefghijklmnopqrstuv',
53
+ },
54
+ {
55
+ category: 'email addresses',
56
+ regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/,
57
+ fires: '{"contact":"ada@example.com"}',
58
+ },
59
+ {
60
+ // Case-sensitive: an IBAN is upper case, and a case-insensitive form
61
+ // matches a run inside a lower-case hex digest.
62
+ category: 'account identifiers',
63
+ regex: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b|\b\d{3}-\d{2}-\d{4}\b/,
64
+ fires: 'GB29NWBK60161331926819',
65
+ },
66
+ {
67
+ category: 'transaction content',
68
+ regex: /(?<![0-9A-Za-z])\d{4}(?:[ -]\d{4}){3}(?![0-9A-Za-z])/,
69
+ fires: 'charged 4111 1111 1111 1111',
70
+ },
71
+ ];
72
+ const step = (path, key) => typeof key === 'number' ? `${path}[${key}]` : `${path}.${key}`;
73
+ /**
74
+ * Every string anywhere inside one value, against every value-shaped pattern.
75
+ *
76
+ * Object keys are walked as paths and never scanned as content: a key is a name
77
+ * by construction, which is the same line the pattern set is drawn on. The walk
78
+ * is total over JSON and stops at nothing, so a token nested under a body
79
+ * example twelve levels down is found; the first matching category per string
80
+ * wins, since one string reported six times says nothing more than once.
81
+ *
82
+ * Order is the walk's order, which for an object is its own key order. A caller
83
+ * that needs a stable report over two spellings of the same object sorts what
84
+ * comes back; the compile gate below reads only the first hit and never the
85
+ * order.
86
+ */
87
+ export function scanExcludedContent(value, path) {
88
+ const hits = [];
89
+ const visit = (node, at) => {
90
+ if (typeof node === 'string') {
91
+ for (const { category, regex } of EXCLUDED_VALUE_PATTERNS) {
92
+ const match = regex.exec(node);
93
+ if (match === null)
94
+ continue;
95
+ hits.push({ category, path: at, match: match[0] });
96
+ return;
97
+ }
98
+ return;
99
+ }
100
+ if (Array.isArray(node)) {
101
+ node.forEach((element, index) => {
102
+ visit(element, step(at, index));
103
+ });
104
+ return;
105
+ }
106
+ if (node === null || typeof node !== 'object')
107
+ return;
108
+ for (const [key, child] of Object.entries(node))
109
+ visit(child, step(at, key));
110
+ };
111
+ visit(value, path);
112
+ return hits;
113
+ }
@@ -1,5 +1,5 @@
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'];
1
+ /** AD-5's twenty-six 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', 'unresolved-artifact-reference', 'irreducible-step-reference', 'excluded-content-in-declaration'];
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-three compile-time failure codes, as data, in the table's order. */
1
+ /** AD-5's twenty-six 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-three members, unique, kebab-case) are locked in
8
+ // (twenty-six 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',
@@ -31,6 +31,9 @@ export const FAILURE_CODES = [
31
31
  'scoped-reference-resolves-forbidden',
32
32
  'waiver-incomplete',
33
33
  'brief-exceeds-scripting-bound',
34
+ 'unresolved-artifact-reference',
35
+ 'irreducible-step-reference',
36
+ 'excluded-content-in-declaration',
34
37
  ];
35
38
  /**
36
39
  * AD-5's compile-time failure registry: thrown when a structural error fails
@@ -67,7 +67,7 @@ export type AgreementField = (typeof AGREEMENT_FIELDS)[number];
67
67
  */
68
68
  export type DuplicateRecordIdentifier = {
69
69
  readonly kind: Extract<IngestConditionKind, 'duplicate-record-identifier'>;
70
- readonly subject: 'observation' | 'finding' | 'oracle-disposition';
70
+ readonly subject: 'observation' | 'finding' | 'oracle-disposition' | 'judge-result';
71
71
  readonly identifier: string;
72
72
  readonly occurrences: number;
73
73
  };
@@ -106,6 +106,14 @@ export const ingest = (record, manifest, configuration) => {
106
106
  'oracle-disposition',
107
107
  record.oracleDispositions.map((disposition) => disposition.oracleId),
108
108
  ],
109
+ // AD-17's "one judge call scoring all named criteria", read as far as
110
+ // one record can be read: a criterion scored twice is not one call's
111
+ // product whatever the rest of the record says. Keyed by the pair,
112
+ // since two rubrics may each declare a criterion of the same name.
113
+ [
114
+ 'judge-result',
115
+ record.judgeResults.map((result) => `${result.rubricId}/${result.criterionId}`),
116
+ ],
109
117
  ]) {
110
118
  const counts = new Map();
111
119
  for (const identifier of identifiers) {
@@ -1,5 +1,6 @@
1
+ import type { ReferenceSetKeys } from '../evaluate/resolution.ts';
1
2
  import type { EvalContract } from '../schemas/eval-contract.ts';
2
- import type { Operation } from '../schemas/interface.ts';
3
+ import type { AnyOperation } from '../schemas/interface.ts';
3
4
  import type { ProbeRequest } from '../schemas/port-messages.ts';
4
5
  import type { JsonValue } from '../schemas/primitives.ts';
5
6
  import type { Probe } from '../schemas/probe.ts';
@@ -15,7 +16,7 @@ export type PlannedLeg = {
15
16
  readonly legId: string;
16
17
  readonly purpose: PlannedLegPurpose;
17
18
  readonly request: ProbeRequest;
18
- readonly operation: Operation;
19
+ readonly operation: AnyOperation;
19
20
  readonly inputs: WitnessInputs;
20
21
  };
21
22
  export type PlannedCheck = {
@@ -28,7 +29,7 @@ export type PlannedCheck = {
28
29
  readonly interfaceId: string;
29
30
  readonly operationId: string;
30
31
  readonly witness: SensitivityWitness | null;
31
- readonly operation: Operation;
32
+ readonly operation: AnyOperation;
32
33
  } | {
33
34
  readonly kind: 'state-reset';
34
35
  readonly legIds: readonly [string, string];
@@ -39,19 +40,20 @@ export type PlannedCheck = {
39
40
  readonly kind: 'seeded-faults-scoped';
40
41
  readonly defectId: string;
41
42
  readonly witness: ManifestationWitness;
42
- readonly operation: Operation;
43
+ readonly operation: AnyOperation;
43
44
  readonly cleanLegIds: readonly string[];
44
45
  } | {
45
46
  readonly kind: 'seeded-fault-fired';
46
47
  readonly defectId: string;
47
48
  readonly witness: ManifestationWitness | null;
48
- readonly operation: Operation | null;
49
+ readonly operation: AnyOperation | null;
49
50
  };
50
51
  export type PreflightPlan = {
51
52
  readonly runId: string;
52
53
  readonly legs: readonly PlannedLeg[];
53
54
  readonly checks: readonly PlannedCheck[];
54
55
  readonly referenceSets: Readonly<Record<string, JsonValue[]>>;
56
+ readonly referenceSetKeys: ReferenceSetKeys;
55
57
  };
56
58
  /**
57
59
  * AD-10's plan, derived from the interfaces the contract's probes exercise.
@@ -8,25 +8,53 @@
8
8
  * the operation and the witness themselves: there is nothing to look an
9
9
  * identifier up in at reduce time.
10
10
  */
11
- import { checkInputsAgainstShape } from '../compile/sensitivity-witness.js';
12
- import { declaresNoRequiredKeys } from '../declared-inputs.js';
11
+ import { checkInputsAgainstShape, isApiWitnessInputs, } from '../compile/sensitivity-witness.js';
12
+ import { declaresNoRequiredKeys, isCommandOperation, } from '../declared-inputs.js';
13
+ import { referenceSetKeysOf } from '../evaluate/evidence-resolution.js';
13
14
  import { StructuralFailure } from '../failure-codes.js';
15
+ import { operationsOf } from '../schemas/interface.js';
14
16
  import { referenceSetMembers } from './witness-evidence.js';
15
- const requestOf = (legId, interfaceId, operation, inputs) => ({
16
- // NFR9's correlation by identifier: the port echoes this back, and it is
17
- // the only thing that tells two legs of one operation apart.
18
- probeId: legId,
19
- interfaceId,
20
- operationId: operation.operationId,
21
- method: operation.method,
22
- pathTemplate: operation.pathTemplate,
23
- channels: {
24
- path: inputs.path,
25
- query: inputs.query,
26
- header: inputs.header,
27
- body: inputs.body,
28
- },
29
- });
17
+ const requestOf = (legId, interfaceId, operation, inputs) => {
18
+ // NFR9's correlation by identifier: the port echoes these back, and they
19
+ // are the only thing that tells two legs of one operation apart.
20
+ const correlation = {
21
+ probeId: legId,
22
+ interfaceId,
23
+ operationId: operation.operationId,
24
+ };
25
+ if (isCommandOperation(operation)) {
26
+ if (isApiWitnessInputs(inputs)) {
27
+ throw new StructuralFailure('undeclared-mandatory-input', `EvalContract.permittedInterfaces[logicalId=${interfaceId}].operations[operationId=${operation.operationId}]`, `leg "${legId}" supplies transport channels to an operation that runs behind a command (AD-10, AD-19)`);
28
+ }
29
+ return {
30
+ ...correlation,
31
+ kind: 'cli',
32
+ executable: operation.invocation.executable,
33
+ subcommandPath: operation.invocation.subcommandPath,
34
+ channels: {
35
+ argument: inputs.argument,
36
+ option: inputs.option,
37
+ environment: inputs.environment,
38
+ stdin: inputs.stdin,
39
+ },
40
+ };
41
+ }
42
+ if (!isApiWitnessInputs(inputs)) {
43
+ throw new StructuralFailure('undeclared-mandatory-input', `EvalContract.permittedInterfaces[logicalId=${interfaceId}].operations[operationId=${operation.operationId}]`, `leg "${legId}" supplies command channels to an operation that speaks HTTP (AD-10, AD-19)`);
44
+ }
45
+ return {
46
+ ...correlation,
47
+ kind: 'api',
48
+ method: operation.method,
49
+ pathTemplate: operation.pathTemplate,
50
+ channels: {
51
+ path: inputs.path,
52
+ query: inputs.query,
53
+ header: inputs.header,
54
+ body: inputs.body,
55
+ },
56
+ };
57
+ };
30
58
  /**
31
59
  * The contract declares no identifier for a control leg, so one is minted, and it
32
60
  * has to avoid every identifier the contract already spends. The same input
@@ -57,13 +85,19 @@ const declaredLegIds = (contract, probes) => {
57
85
  taken.add(legId);
58
86
  }
59
87
  for (const iface of contract.permittedInterfaces)
60
- for (const operation of iface.operations)
88
+ for (const operation of operationsOf(iface))
61
89
  for (const leg of operation.sensitivityWitness?.legs ?? [])
62
90
  taken.add(leg.legId);
63
91
  if (contract.fixtureReset !== null)
64
92
  taken.add(contract.fixtureReset.legId);
65
93
  return taken;
66
94
  };
95
+ const EMPTY_COMMAND_INPUTS = {
96
+ argument: {},
97
+ option: {},
98
+ environment: {},
99
+ stdin: { kind: 'absent' },
100
+ };
67
101
  const EMPTY_INPUTS = {
68
102
  path: {},
69
103
  query: {},
@@ -85,7 +119,12 @@ const controlInputs = (operation) => {
85
119
  const declared = operation.sensitivityWitness?.legs[0]?.inputs;
86
120
  if (declared !== undefined)
87
121
  return declared;
88
- return declaresNoRequiredKeys(operation) ? EMPTY_INPUTS : null;
122
+ // AD-10 exempts an operation with no required key, so an empty leg is a
123
+ // legal request for it. Which empty leg depends on the kind, because the
124
+ // port sends what the operation's own channels name.
125
+ if (!declaresNoRequiredKeys(operation))
126
+ return null;
127
+ return isCommandOperation(operation) ? EMPTY_COMMAND_INPUTS : EMPTY_INPUTS;
89
128
  };
90
129
  const targetOf = (iface, operation) => {
91
130
  const inputs = controlInputs(operation);
@@ -98,10 +137,10 @@ const targetOf = (iface, operation) => {
98
137
  * marker-true operation on the interface the fixture reset names, resolved
99
138
  * independently of where the observed read came from.
100
139
  */
101
- const selectControl = (contract) => {
140
+ const selectControl = (contract, permittedInterfaces) => {
102
141
  let observed = null;
103
- for (const iface of contract.permittedInterfaces) {
104
- for (const operation of iface.operations) {
142
+ for (const iface of permittedInterfaces) {
143
+ for (const operation of operationsOf(iface)) {
105
144
  if (operation.stateChangeMarker)
106
145
  continue;
107
146
  observed = targetOf(iface, operation);
@@ -116,11 +155,13 @@ const selectControl = (contract) => {
116
155
  const reset = contract.fixtureReset;
117
156
  if (reset === null)
118
157
  return { observed, mutating: null };
119
- const resetInterface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
158
+ const resetInterface = permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
120
159
  let mutating = null;
121
160
  for (const operation of resetInterface?.operations ?? []) {
122
161
  if (!operation.stateChangeMarker)
123
162
  continue;
163
+ if (resetInterface === undefined)
164
+ break;
124
165
  mutating = targetOf(resetInterface, operation);
125
166
  if (mutating !== null)
126
167
  break;
@@ -141,12 +182,13 @@ const selectControl = (contract) => {
141
182
  export const planPreflight = (input) => {
142
183
  const { contract, probes, runId } = input;
143
184
  for (const iface of contract.permittedInterfaces) {
144
- // Already thrown at compile; asserted again because the plan is reachable
145
- // from a caller who assembled a contract by hand.
146
- if (iface.kind !== 'api') {
147
- throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported in v0; only "api" is (AD-10)`);
185
+ // Already thrown at compile; asserted again because the plan is
186
+ // reachable from a caller who assembled a contract by hand.
187
+ if (iface.kind !== 'api' && iface.kind !== 'cli') {
188
+ throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported; "api" and "cli" are (AD-10)`);
148
189
  }
149
190
  }
191
+ const permittedInterfaces = contract.permittedInterfaces;
150
192
  const origins = [];
151
193
  const checks = [];
152
194
  // Keyed by interface AND operation: `Operation.operationId` is scoped to a
@@ -175,8 +217,8 @@ export const planPreflight = (input) => {
175
217
  group.push(legId);
176
218
  };
177
219
  // 1. the sensitivity legs and their checks
178
- contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
179
- iface.operations.forEach((operation, operationIndex) => {
220
+ permittedInterfaces.forEach((iface, interfaceIndex) => {
221
+ operationsOf(iface).forEach((operation, operationIndex) => {
180
222
  const witness = operation.sensitivityWitness;
181
223
  const path = `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
182
224
  if (witness !== null)
@@ -193,7 +235,7 @@ export const planPreflight = (input) => {
193
235
  });
194
236
  });
195
237
  // 3. the control legs, once per contract
196
- const control = selectControl(contract);
238
+ const control = selectControl(contract, permittedInterfaces);
197
239
  const controlLegIds = [];
198
240
  if (control !== null) {
199
241
  const taken = declaredLegIds(contract, probes);
@@ -215,7 +257,7 @@ export const planPreflight = (input) => {
215
257
  const mutate = mintControl('preflight-control-mutate');
216
258
  addLeg(mutate, 'control-mutate', mutating.iface.logicalId, mutating.operation, mutating.inputs, `the minted control-mutate leg against "${mutating.operation.operationId}"`);
217
259
  controlLegIds.push(mutate);
218
- const resetOperation = mutating.iface.operations.find((candidate) => candidate.operationId === reset.operationId);
260
+ const resetOperation = operationsOf(mutating.iface).find((candidate) => candidate.operationId === reset.operationId);
219
261
  if (resetOperation === undefined) {
220
262
  throw new StructuralFailure('unreachable-check-evidence', 'EvalContract.fixtureReset', `the fixture reset names operation "${reset.operationId}" on interface "${reset.interfaceId}", which the contract does not declare (AD-10)`);
221
263
  }
@@ -246,7 +288,7 @@ export const planPreflight = (input) => {
246
288
  });
247
289
  continue;
248
290
  }
249
- const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === witness.interfaceId);
291
+ const iface = permittedInterfaces.find((candidate) => candidate.logicalId === witness.interfaceId);
250
292
  const operation = iface?.operations.find((candidate) => candidate.operationId === witness.operationId);
251
293
  if (operation === undefined) {
252
294
  throw new StructuralFailure('unreachable-check-evidence', path, `the manifestation witness names operation "${witness.operationId}" on interface "${witness.interfaceId}", which the contract does not declare (AD-10)`);
@@ -309,5 +351,6 @@ export const planPreflight = (input) => {
309
351
  legs: origins.map((origin) => origin.leg),
310
352
  checks: [...presence, ...checks],
311
353
  referenceSets: referenceSetMembers(contract),
354
+ referenceSetKeys: referenceSetKeysOf(contract),
312
355
  };
313
356
  };
@@ -1,4 +1,4 @@
1
- import type { Operation } from '../schemas/interface.ts';
1
+ import type { AnyOperation } from '../schemas/interface.ts';
2
2
  import type { ProbeObservation, ProbeObservedBody } from '../schemas/port-messages.ts';
3
3
  export declare const PREFLIGHT_ARTIFACT_PATH = "PreflightVerdict";
4
4
  /**
@@ -17,7 +17,10 @@ export type ProjectedObservation = {
17
17
  readonly legId: string;
18
18
  readonly interfaceId: string;
19
19
  readonly operationId: string;
20
- readonly status: number;
20
+ /** the transport status, or `null` off an interface that speaks HTTP. */
21
+ readonly status: number | null;
22
+ /** the process exit code, or `null` on an interface that does. */
23
+ readonly exitCode: number | null;
21
24
  readonly body: ProbeObservedBody;
22
25
  };
23
26
  /**
@@ -27,7 +30,7 @@ export type ProjectedObservation = {
27
30
  */
28
31
  export declare function pruneVolatile(body: ProbeObservedBody, volatilePointers: readonly string[], artifactPath: string): ProbeObservedBody;
29
32
  /** One observation reduced to the projection, with its volatile pointers gone. */
30
- export declare function projectObservation(observation: ProbeObservation, operation: Operation, artifactPath: string): ProjectedObservation;
33
+ export declare function projectObservation(observation: ProbeObservation, operation: AnyOperation, artifactPath: string): ProjectedObservation;
31
34
  /**
32
35
  * AD-11's fixture digest. Sorted by leg id first, because NFR9 forbids any
33
36
  * stage from reading array position: two runs whose observations arrived in a
@@ -4,6 +4,7 @@
4
4
  * scoring version: two runs whose projections agree describe the same fixture.
5
5
  */
6
6
  import { digestComposite } from '../canonical/digest.js';
7
+ import { descriptorArtifactOf, descriptorChannelOf, } from '../declared-inputs.js';
7
8
  import { DESCRIPTOR_POINTER_PATTERN } from '../schemas/pointer.js';
8
9
  import { decodeTail } from '../seal/plan-index.js';
9
10
  export const PREFLIGHT_ARTIFACT_PATH = 'PreflightVerdict';
@@ -70,14 +71,33 @@ export function pruneVolatile(body, volatilePointers, artifactPath) {
70
71
  }
71
72
  /** One observation reduced to the projection, with its volatile pointers gone. */
72
73
  export function projectObservation(observation, operation, artifactPath) {
74
+ // The channel the operation's descriptor describes is the one AD-10's
75
+ // relation reads, so it is the one the volatile pointers are pruned from.
76
+ // Off an interface that speaks HTTP that is the response body; off a
77
+ // command it is the stream or the file the operation nominates.
78
+ const observed = observation.kind === 'api'
79
+ ? observation.body
80
+ : describedChannelOf(observation, operation);
73
81
  return {
74
82
  legId: observation.probeId,
75
83
  interfaceId: observation.interfaceId,
76
84
  operationId: observation.operationId,
77
- status: observation.status,
78
- body: pruneVolatile(observation.body, operation.volatilePointers, artifactPath),
85
+ // A command has no transport status. `null` is what a reader of this
86
+ // field asks about, and every such reader already handles it.
87
+ status: observation.kind === 'api' ? observation.status : null,
88
+ exitCode: observation.kind === 'api' ? null : observation.exitCode,
89
+ body: pruneVolatile(observed, operation.volatilePointers, artifactPath),
79
90
  };
80
91
  }
92
+ /** The observed value of whichever channel this operation's descriptor describes. */
93
+ function describedChannelOf(observation, operation) {
94
+ const artifactId = descriptorArtifactOf(operation);
95
+ if (artifactId !== null)
96
+ return observation.artifacts[artifactId] ?? { kind: 'absent' };
97
+ return descriptorChannelOf(operation) === 'stderr'
98
+ ? observation.stderr
99
+ : observation.stdout;
100
+ }
81
101
  /**
82
102
  * AD-11's fixture digest. Sorted by leg id first, because NFR9 forbids any
83
103
  * stage from reading array position: two runs whose observations arrived in a
@@ -16,10 +16,39 @@ import { evidenceOf, resolveWitnessRelation } from './witness-evidence.js';
16
16
  /**
17
17
  * AD-10 names no threshold, and this is the one the repository already speaks:
18
18
  * the published conformance suite ships `probe/observe-anomalous-status`, and
19
- * `ProbeObservation.status` is bounded to 100-599 at the port, so HTTP is
20
- * already assumed at that boundary.
19
+ * an api observation's `status` is bounded to 100-599 at the port.
21
20
  */
22
21
  const ANOMALOUS_STATUS = 400;
22
+ /**
23
+ * What makes a control leg anomalous, in the vocabulary of the kind it ran
24
+ * against, or `null` when nothing does. A command's analogue of a 4xx is a
25
+ * non-zero exit: both are the system saying the call did not go through, and
26
+ * AD-10's clean-control check is about exactly that.
27
+ */
28
+ function anomalyOf(observation) {
29
+ if (observation.kind === 'api') {
30
+ return observation.status >= ANOMALOUS_STATUS
31
+ ? `status ${observation.status}`
32
+ : null;
33
+ }
34
+ return observation.exitCode === 0 ? null : `exit code ${observation.exitCode}`;
35
+ }
36
+ /**
37
+ * Whether the observation answers the mechanism the leg asked about.
38
+ *
39
+ * `kind` alone, and deliberately not `interfaceId` or `operationId`: those two
40
+ * are already read by the `interface-present` check, which reports a mismatch
41
+ * as a failed verdict rather than a fault, and reclassifying them here would
42
+ * turn a shipped verdict into a thrown fault. `kind` has no such reader. An
43
+ * observation of the wrong mechanism is not a weaker answer to the question, it
44
+ * is an answer to a different one, and every check below would read it as the
45
+ * leg's own result.
46
+ */
47
+ function kindMismatch(leg, observation) {
48
+ if (leg.request.kind === observation.kind)
49
+ return undefined;
50
+ return `leg "${leg.legId}" asked for a "${leg.request.kind}" probe and was answered with a "${observation.kind}" observation, so the port answered a question nobody asked`;
51
+ }
23
52
  const check = (kind, operationId, outcome, note) => ({ kind, operationId, outcome, note });
24
53
  /**
25
54
  * Two projections describe the same fixture state. `legId` is excluded because
@@ -48,7 +77,7 @@ const resolveAgainst = (witness, state, plan, artifactPath) => {
48
77
  // Keyed by the witness's own leg id, since the relation addresses
49
78
  // `/interactions/{witness.legId}/…`. The scoped row asks whether that same
50
79
  // relation would fire on a clean leg's observation.
51
- return resolveWitnessRelation(witness.relation, { [witness.legId]: state.evidence }, state.leg.operation, plan.referenceSets, artifactPath).resolution;
80
+ return resolveWitnessRelation(witness.relation, { [witness.legId]: state.evidence }, state.leg.operation, plan.referenceSets, plan.referenceSetKeys, artifactPath).resolution;
52
81
  };
53
82
  export const reducePreflight = (plan, { observations }) => {
54
83
  const byProbeId = new Map();
@@ -68,12 +97,22 @@ export const reducePreflight = (plan, { observations }) => {
68
97
  const observation = byProbeId.get(leg.legId);
69
98
  if (observation === undefined)
70
99
  continue;
100
+ // The echo is checked, not assumed. Both port messages are unions, so a
101
+ // port can answer a command leg with a schema-valid HTTP observation and
102
+ // every check below reads it as the leg's own result: a command contract
103
+ // then passes pre-flight with four checks satisfied and no command ever
104
+ // run. `probeId` alone binds the two together and says nothing about
105
+ // whether the answer is to this question.
106
+ const mismatch = kindMismatch(leg, observation);
107
+ if (mismatch !== undefined) {
108
+ throw new RuntimeFault('port-contract-violation', PREFLIGHT_ARTIFACT_PATH, mismatch);
109
+ }
71
110
  const projected = projectObservation(observation, leg.operation, PREFLIGHT_ARTIFACT_PATH);
72
111
  states.set(leg.legId, {
73
112
  leg,
74
113
  observation,
75
114
  projected,
76
- evidence: evidenceOf(projected, observation, leg.inputs),
115
+ evidence: evidenceOf(projected, observation, leg.inputs, leg.operation),
77
116
  });
78
117
  }
79
118
  const reduceCheck = (planned) => {
@@ -105,7 +144,7 @@ export const reducePreflight = (plan, { observations }) => {
105
144
  return check(planned.kind, planned.operationId, 'failed', `witness leg "${leg.legId}" produced no observation`);
106
145
  evidence[leg.legId] = state.evidence;
107
146
  }
108
- const { resolution } = resolveWitnessRelation(witness.relation, evidence, operation, plan.referenceSets, PREFLIGHT_ARTIFACT_PATH);
147
+ const { resolution } = resolveWitnessRelation(witness.relation, evidence, operation, plan.referenceSets, plan.referenceSetKeys, PREFLIGHT_ARTIFACT_PATH);
109
148
  // AD-10's own sentence, and the most load-bearing line here: a
110
149
  // sensitivity check that examined nothing has established nothing,
111
150
  // so `insufficient-evidence` fails.
@@ -128,8 +167,9 @@ export const reducePreflight = (plan, { observations }) => {
128
167
  const state = states.get(legId);
129
168
  if (state === undefined)
130
169
  return check(planned.kind, null, 'failed', `control leg "${legId}" produced no observation`);
131
- if (state.observation.status >= ANOMALOUS_STATUS)
132
- return check(planned.kind, null, 'failed', `control leg "${legId}" observed status ${state.observation.status}`);
170
+ const anomaly = anomalyOf(state.observation);
171
+ if (anomaly !== null)
172
+ return check(planned.kind, null, 'failed', `control leg "${legId}" observed ${anomaly}`);
133
173
  }
134
174
  return check(planned.kind, null, 'satisfied', null);
135
175
  }
@@ -4,11 +4,11 @@
4
4
  * unchanged. This module supplies the `Observation` shape they read and a
5
5
  * collection predicate scoped to one operation.
6
6
  */
7
- import { type PointerDenotesCollection } from '../evaluate/resolution.ts';
7
+ import { type PointerDenotesCollection, type ReferenceSetKeys } from '../evaluate/resolution.ts';
8
8
  import type { EvalContract } from '../schemas/eval-contract.ts';
9
9
  import type { CheckResolutionValue } from '../schemas/evidence-artifact.ts';
10
10
  import type { Expression } from '../schemas/expression.ts';
11
- import type { Operation } from '../schemas/interface.ts';
11
+ import type { AnyOperation } from '../schemas/interface.ts';
12
12
  import type { ProbeObservation } from '../schemas/port-messages.ts';
13
13
  import type { JsonValue } from '../schemas/primitives.ts';
14
14
  import type { Observation } from '../schemas/sealed-run-record.ts';
@@ -27,7 +27,7 @@ export declare const PREFLIGHT_REGEX_MATCH_STEP_BUDGET = 1000000;
27
27
  * the projection does not carry. `provenance` is `baseline`: a pre-flight leg is
28
28
  * pre-canned by definition.
29
29
  */
30
- export declare function evidenceOf(projected: ProjectedObservation, observation: ProbeObservation, inputs: WitnessInputs): Observation;
30
+ export declare function evidenceOf(projected: ProjectedObservation, observation: ProbeObservation, inputs: WitnessInputs, operation: AnyOperation): Observation;
31
31
  /** the contract's declared reference sets in the shape the resolver wants. */
32
32
  export declare function referenceSetMembers(contract: EvalContract): Readonly<Record<string, JsonValue[]>>;
33
33
  /**
@@ -37,6 +37,6 @@ export declare function referenceSetMembers(contract: EvalContract): Readonly<Re
37
37
  * never appears, and fabricating step objects for a one-operation lookup is more
38
38
  * machinery than the predicate.
39
39
  */
40
- export declare function makeWitnessPointerDenotesCollection(operation: Operation): PointerDenotesCollection;
40
+ export declare function makeWitnessPointerDenotesCollection(operation: AnyOperation): PointerDenotesCollection;
41
41
  /** Resolves one witness relation over the legs it addresses. */
42
- export declare function resolveWitnessRelation(relation: Expression, legEvidence: Readonly<Record<string, Observation>>, operation: Operation, referenceSets: Readonly<Record<string, JsonValue[]>>, artifactPath: string): CheckResolutionValue;
42
+ export declare function resolveWitnessRelation(relation: Expression, legEvidence: Readonly<Record<string, Observation>>, operation: AnyOperation, referenceSets: Readonly<Record<string, JsonValue[]>>, referenceSetKeys: ReferenceSetKeys, artifactPath: string): CheckResolutionValue;