eval-quality 0.1.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 (230) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +355 -0
  3. package/corpus/dev/README.md +36 -0
  4. package/corpus/dev/compile-seal-example/brief.json +1 -0
  5. package/corpus/dev/compile-seal-example/contract.json +1 -0
  6. package/corpus/dev/contracts/absent-collection-locations.json +1 -0
  7. package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
  8. package/corpus/dev/contracts/absent-success-indicator.json +1 -0
  9. package/corpus/dev/contracts/empty-channel-roles.json +1 -0
  10. package/corpus/dev/contracts/empty-collection-locations.json +1 -0
  11. package/corpus/dev/contracts/empty-request-shapes.json +1 -0
  12. package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -0
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -0
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -0
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -0
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
  19. package/corpus/dev/contracts/satisfied-declarations.json +1 -0
  20. package/corpus/dev/contracts/single-required-response-key.json +1 -0
  21. package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
  22. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
  23. package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
  24. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
  25. package/corpus/dev/index.json +1 -0
  26. package/dist/adapters/index.d.ts +12 -0
  27. package/dist/adapters/index.js +3 -0
  28. package/dist/adapters/local-corpus-adapter.d.ts +7 -0
  29. package/dist/adapters/local-corpus-adapter.js +78 -0
  30. package/dist/adapters/node-file-system-adapter.d.ts +7 -0
  31. package/dist/adapters/node-file-system-adapter.js +39 -0
  32. package/dist/adapters/port-boundary.d.ts +18 -0
  33. package/dist/adapters/port-boundary.js +83 -0
  34. package/dist/adapters/system-clock-adapter.d.ts +10 -0
  35. package/dist/adapters/system-clock-adapter.js +18 -0
  36. package/dist/application/compile.d.ts +4 -0
  37. package/dist/application/compile.js +24 -0
  38. package/dist/application/diagnostics.d.ts +22 -0
  39. package/dist/application/diagnostics.js +18 -0
  40. package/dist/application/index.d.ts +23 -0
  41. package/dist/application/index.js +17 -0
  42. package/dist/application/invoke-port.d.ts +2 -0
  43. package/dist/application/invoke-port.js +45 -0
  44. package/dist/application/preflight.d.ts +27 -0
  45. package/dist/application/preflight.js +131 -0
  46. package/dist/application/seal.d.ts +4 -0
  47. package/dist/application/seal.js +28 -0
  48. package/dist/application/serialize.d.ts +6 -0
  49. package/dist/application/serialize.js +9 -0
  50. package/dist/cli/arguments.d.ts +28 -0
  51. package/dist/cli/arguments.js +167 -0
  52. package/dist/cli/exit-codes.d.ts +36 -0
  53. package/dist/cli/exit-codes.js +39 -0
  54. package/dist/cli/main.d.ts +2 -0
  55. package/dist/cli/main.js +143 -0
  56. package/dist/cli/render.d.ts +26 -0
  57. package/dist/cli/render.js +53 -0
  58. package/dist/cli/run.d.ts +41 -0
  59. package/dist/cli/run.js +203 -0
  60. package/dist/core/canonical/canonicalize.d.ts +1 -0
  61. package/dist/core/canonical/canonicalize.js +113 -0
  62. package/dist/core/canonical/digest.d.ts +7 -0
  63. package/dist/core/canonical/digest.js +81 -0
  64. package/dist/core/canonical/scan-json.d.ts +1 -0
  65. package/dist/core/canonical/scan-json.js +298 -0
  66. package/dist/core/canonical/value-domain.d.ts +4 -0
  67. package/dist/core/canonical/value-domain.js +113 -0
  68. package/dist/core/compile/compile.d.ts +24 -0
  69. package/dist/core/compile/compile.js +67 -0
  70. package/dist/core/compile/declarations.d.ts +5 -0
  71. package/dist/core/compile/declarations.js +22 -0
  72. package/dist/core/compile/expression-legality.d.ts +14 -0
  73. package/dist/core/compile/expression-legality.js +346 -0
  74. package/dist/core/compile/forbidden-inputs.d.ts +5 -0
  75. package/dist/core/compile/forbidden-inputs.js +19 -0
  76. package/dist/core/compile/interface-inventory.d.ts +10 -0
  77. package/dist/core/compile/interface-inventory.js +55 -0
  78. package/dist/core/compile/oracle-alignment.d.ts +14 -0
  79. package/dist/core/compile/oracle-alignment.js +93 -0
  80. package/dist/core/compile/reachability.d.ts +17 -0
  81. package/dist/core/compile/reachability.js +177 -0
  82. package/dist/core/compile/rubrics.d.ts +55 -0
  83. package/dist/core/compile/rubrics.js +198 -0
  84. package/dist/core/compile/scripting-bound.d.ts +5 -0
  85. package/dist/core/compile/scripting-bound.js +151 -0
  86. package/dist/core/compile/sensitivity-witness.d.ts +73 -0
  87. package/dist/core/compile/sensitivity-witness.js +232 -0
  88. package/dist/core/compile/waivers.d.ts +3 -0
  89. package/dist/core/compile/waivers.js +21 -0
  90. package/dist/core/coverage/coverage.d.ts +18 -0
  91. package/dist/core/coverage/coverage.js +47 -0
  92. package/dist/core/coverage/relevance.d.ts +75 -0
  93. package/dist/core/coverage/relevance.js +186 -0
  94. package/dist/core/coverage/rules.d.ts +7 -0
  95. package/dist/core/coverage/rules.js +18 -0
  96. package/dist/core/coverage/satisfaction.d.ts +107 -0
  97. package/dist/core/coverage/satisfaction.js +415 -0
  98. package/dist/core/coverage/table.d.ts +26 -0
  99. package/dist/core/coverage/table.js +222 -0
  100. package/dist/core/declared-inputs.d.ts +16 -0
  101. package/dist/core/declared-inputs.js +18 -0
  102. package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
  103. package/dist/core/evaluate/evidence-resolution.js +153 -0
  104. package/dist/core/evaluate/operators.d.ts +87 -0
  105. package/dist/core/evaluate/operators.js +350 -0
  106. package/dist/core/evaluate/resolution.d.ts +40 -0
  107. package/dist/core/evaluate/resolution.js +336 -0
  108. package/dist/core/evaluate/resolved-value.d.ts +9 -0
  109. package/dist/core/evaluate/resolved-value.js +6 -0
  110. package/dist/core/failure-codes.d.ts +18 -0
  111. package/dist/core/failure-codes.js +50 -0
  112. package/dist/core/lineage/chain.d.ts +49 -0
  113. package/dist/core/lineage/chain.js +210 -0
  114. package/dist/core/lineage/freeze.d.ts +12 -0
  115. package/dist/core/lineage/freeze.js +35 -0
  116. package/dist/core/lineage/stage-table.d.ts +43 -0
  117. package/dist/core/lineage/stage-table.js +106 -0
  118. package/dist/core/preflight/plan.d.ts +67 -0
  119. package/dist/core/preflight/plan.js +313 -0
  120. package/dist/core/preflight/projection.d.ts +40 -0
  121. package/dist/core/preflight/projection.js +96 -0
  122. package/dist/core/preflight/reduce.d.ts +8 -0
  123. package/dist/core/preflight/reduce.js +180 -0
  124. package/dist/core/preflight/witness-evidence.d.ts +42 -0
  125. package/dist/core/preflight/witness-evidence.js +88 -0
  126. package/dist/core/probe/target-policy.d.ts +63 -0
  127. package/dist/core/probe/target-policy.js +328 -0
  128. package/dist/core/schemas/artifact-reference.d.ts +19 -0
  129. package/dist/core/schemas/artifact-reference.js +30 -0
  130. package/dist/core/schemas/artifact.d.ts +1153 -0
  131. package/dist/core/schemas/artifact.js +84 -0
  132. package/dist/core/schemas/constraint-ledger.d.ts +57 -0
  133. package/dist/core/schemas/constraint-ledger.js +143 -0
  134. package/dist/core/schemas/eval-contract.d.ts +402 -0
  135. package/dist/core/schemas/eval-contract.js +147 -0
  136. package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
  137. package/dist/core/schemas/evaluator-configuration.js +43 -0
  138. package/dist/core/schemas/evidence-artifact.d.ts +471 -0
  139. package/dist/core/schemas/evidence-artifact.js +232 -0
  140. package/dist/core/schemas/expression.d.ts +197 -0
  141. package/dist/core/schemas/expression.js +284 -0
  142. package/dist/core/schemas/faults.d.ts +9 -0
  143. package/dist/core/schemas/faults.js +32 -0
  144. package/dist/core/schemas/interface.d.ts +417 -0
  145. package/dist/core/schemas/interface.js +115 -0
  146. package/dist/core/schemas/isolation-manifest.d.ts +136 -0
  147. package/dist/core/schemas/isolation-manifest.js +92 -0
  148. package/dist/core/schemas/lineage.d.ts +21 -0
  149. package/dist/core/schemas/lineage.js +28 -0
  150. package/dist/core/schemas/oracle.d.ts +72 -0
  151. package/dist/core/schemas/oracle.js +35 -0
  152. package/dist/core/schemas/plan.d.ts +115 -0
  153. package/dist/core/schemas/plan.js +58 -0
  154. package/dist/core/schemas/pointer.d.ts +63 -0
  155. package/dist/core/schemas/pointer.js +94 -0
  156. package/dist/core/schemas/port-messages.d.ts +103 -0
  157. package/dist/core/schemas/port-messages.js +90 -0
  158. package/dist/core/schemas/preflight-verdict.d.ts +60 -0
  159. package/dist/core/schemas/preflight-verdict.js +38 -0
  160. package/dist/core/schemas/primitives.d.ts +78 -0
  161. package/dist/core/schemas/primitives.js +121 -0
  162. package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
  163. package/dist/core/schemas/private-artifact-manifest.js +46 -0
  164. package/dist/core/schemas/probe-body.d.ts +25 -0
  165. package/dist/core/schemas/probe-body.js +23 -0
  166. package/dist/core/schemas/probe-policy.d.ts +76 -0
  167. package/dist/core/schemas/probe-policy.js +33 -0
  168. package/dist/core/schemas/probe.d.ts +189 -0
  169. package/dist/core/schemas/probe.js +82 -0
  170. package/dist/core/schemas/publish.d.ts +30 -0
  171. package/dist/core/schemas/publish.js +135 -0
  172. package/dist/core/schemas/reference-set.d.ts +18 -0
  173. package/dist/core/schemas/reference-set.js +27 -0
  174. package/dist/core/schemas/rubric.d.ts +62 -0
  175. package/dist/core/schemas/rubric.js +68 -0
  176. package/dist/core/schemas/scoring-policy.d.ts +28 -0
  177. package/dist/core/schemas/scoring-policy.js +51 -0
  178. package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
  179. package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
  180. package/dist/core/schemas/sealed-run-record.d.ts +382 -0
  181. package/dist/core/schemas/sealed-run-record.js +215 -0
  182. package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
  183. package/dist/core/schemas/sensitivity-witness.js +86 -0
  184. package/dist/core/schemas/verdict.d.ts +25 -0
  185. package/dist/core/schemas/verdict.js +20 -0
  186. package/dist/core/schemas/waiver.d.ts +17 -0
  187. package/dist/core/schemas/waiver.js +24 -0
  188. package/dist/core/seal/derived-reference.d.ts +36 -0
  189. package/dist/core/seal/derived-reference.js +383 -0
  190. package/dist/core/seal/direction-prose.d.ts +13 -0
  191. package/dist/core/seal/direction-prose.js +120 -0
  192. package/dist/core/seal/plan-index.d.ts +63 -0
  193. package/dist/core/seal/plan-index.js +155 -0
  194. package/dist/core/seal/scripting-audit.d.ts +15 -0
  195. package/dist/core/seal/scripting-audit.js +51 -0
  196. package/dist/core/seal/seal.d.ts +18 -0
  197. package/dist/core/seal/seal.js +155 -0
  198. package/dist/core/stage-contracts.d.ts +30 -0
  199. package/dist/core/stage-contracts.js +1 -0
  200. package/dist/index.d.ts +15 -0
  201. package/dist/index.js +22 -0
  202. package/dist/ports/clock-port.d.ts +13 -0
  203. package/dist/ports/clock-port.js +7 -0
  204. package/dist/ports/corpus-port.d.ts +16 -0
  205. package/dist/ports/corpus-port.js +7 -0
  206. package/dist/ports/environment-probe-port.d.ts +76 -0
  207. package/dist/ports/environment-probe-port.js +32 -0
  208. package/dist/ports/file-system-port.d.ts +27 -0
  209. package/dist/ports/file-system-port.js +11 -0
  210. package/dist/ports/port.d.ts +33 -0
  211. package/dist/ports/port.js +14 -0
  212. package/dist/testing/conformance.d.ts +88 -0
  213. package/dist/testing/conformance.js +281 -0
  214. package/dist/testing/index.d.ts +26 -0
  215. package/dist/testing/index.js +18 -0
  216. package/dist/testing/probe-conformance.d.ts +45 -0
  217. package/dist/testing/probe-conformance.js +186 -0
  218. package/package.json +130 -0
  219. package/schemas/artifact-reference.schema.json +64 -0
  220. package/schemas/eval-contract.schema.json +2145 -0
  221. package/schemas/evaluator-configuration.schema.json +199 -0
  222. package/schemas/evidence-artifact.schema.json +1229 -0
  223. package/schemas/isolation-manifest.schema.json +394 -0
  224. package/schemas/preflight-verdict.schema.json +114 -0
  225. package/schemas/private-artifact-manifest.schema.json +103 -0
  226. package/schemas/probe.schema.json +1151 -0
  227. package/schemas/rubric.schema.json +148 -0
  228. package/schemas/scoring-policy.schema.json +90 -0
  229. package/schemas/sealed-evaluator-brief.schema.json +268 -0
  230. package/schemas/sealed-run-record.schema.json +875 -0
@@ -0,0 +1,27 @@
1
+ import { EvalContract } from '../core/schemas/eval-contract.ts';
2
+ import { ProbeObservation } from '../core/schemas/port-messages.ts';
3
+ import { PreflightVerdict } from '../core/schemas/preflight-verdict.ts';
4
+ import { Probe } from '../core/schemas/probe.ts';
5
+ import { type EnvironmentProbePort } from '../ports/environment-probe-port.ts';
6
+ import { type DiagnosticSink } from './diagnostics.ts';
7
+ export type RunPreflightOptions = {
8
+ readonly contract: EvalContract;
9
+ readonly probes: readonly Probe[];
10
+ readonly runId: string;
11
+ readonly port: EnvironmentProbePort;
12
+ readonly signal: AbortSignal;
13
+ readonly sink?: DiagnosticSink;
14
+ };
15
+ export type PreflightFromObservationsOptions = {
16
+ readonly contract: EvalContract;
17
+ readonly probes: readonly Probe[];
18
+ readonly runId: string;
19
+ readonly observations: readonly ProbeObservation[];
20
+ readonly sink?: DiagnosticSink;
21
+ };
22
+ export declare function runPreflight(options: RunPreflightOptions): Promise<PreflightVerdict>;
23
+ /**
24
+ * The same pre-flight verdict for a caller who probed by some other means:
25
+ * plans, skips the awaiting, reduces over observations it is handed.
26
+ */
27
+ export declare function preflightFromObservations(options: PreflightFromObservationsOptions): PreflightVerdict;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The one place a pre-flight probe is awaited (AD-34: `application/` is the
3
+ * only layer that awaits). Parses its inputs, plans, issues every leg through
4
+ * the environment-probe port in plan order, reduces, and parses the verdict.
5
+ *
6
+ * `preflightFromObservations` is the same verdict for a caller that probed by
7
+ * some other means: it plans, skips the awaiting, and reduces over the
8
+ * observations it is handed. Both entry points land on one `reducePreflight`
9
+ * and return one `PreflightVerdict`.
10
+ *
11
+ * No decision logic lives here: every branch is a parse, an await, or a rethrow.
12
+ * A failed pre-flight is a verdict and a failed probe is a fault, so a
13
+ * `RuntimeFault` from a leg propagates.
14
+ */
15
+ import { freezeArtifact } from '../core/lineage/freeze.js';
16
+ import { planPreflight } from '../core/preflight/plan.js';
17
+ import { reducePreflight } from '../core/preflight/reduce.js';
18
+ import { EvalContract } from '../core/schemas/eval-contract.js';
19
+ import { RuntimeFault } from '../core/schemas/faults.js';
20
+ import { ProbeObservation } from '../core/schemas/port-messages.js';
21
+ import { PreflightVerdict } from '../core/schemas/preflight-verdict.js';
22
+ import { Probe } from '../core/schemas/probe.js';
23
+ import { probeParsers, } from '../ports/environment-probe-port.js';
24
+ import { emit } from './diagnostics.js';
25
+ import { invokePort } from './invoke-port.js';
26
+ /** Artifacts are validated in both directions (AD-28), probes included. */
27
+ function parseProbes(input) {
28
+ const parsed = Probe.array().safeParse(input);
29
+ if (!parsed.success) {
30
+ throw new RuntimeFault('schema-parse-failure', 'Probe', 'input does not conform to the Probe schema', { cause: parsed.error });
31
+ }
32
+ return parsed.data;
33
+ }
34
+ function parseContract(input) {
35
+ const parsed = EvalContract.safeParse(input);
36
+ if (!parsed.success) {
37
+ throw new RuntimeFault('schema-parse-failure', 'EvalContract', 'input does not conform to the EvalContract schema', { cause: parsed.error });
38
+ }
39
+ return parsed.data;
40
+ }
41
+ function parseObservations(input) {
42
+ const parsed = ProbeObservation.array().safeParse(input);
43
+ if (!parsed.success) {
44
+ throw new RuntimeFault('schema-parse-failure', 'ProbeObservation', 'input does not conform to the ProbeObservation schema', { cause: parsed.error });
45
+ }
46
+ return parsed.data;
47
+ }
48
+ /**
49
+ * Frozen again after the outbound parse: `safeParse` returns a fresh object,
50
+ * so the core stage's freeze does not survive boundary validation.
51
+ */
52
+ function parseVerdict(verdict) {
53
+ const parsed = PreflightVerdict.safeParse(verdict);
54
+ if (!parsed.success) {
55
+ throw new RuntimeFault('schema-parse-failure', 'PreflightVerdict', 'the reduced verdict does not conform to the PreflightVerdict schema', { cause: parsed.error });
56
+ }
57
+ return freezeArtifact(parsed.data);
58
+ }
59
+ const plannedLine = (legId) => `leg "${legId}": planned`;
60
+ const observedLine = (legId) => `leg "${legId}": observed`;
61
+ const closingLine = (legCount, passed) => `reduced ${legCount} leg(s): ${passed ? 'passed' : 'failed'}`;
62
+ export async function runPreflight(options) {
63
+ const { runId, port, signal, sink } = options;
64
+ const parsedContract = parseContract(options.contract);
65
+ const parsedProbes = parseProbes(options.probes);
66
+ const plan = planPreflight({
67
+ contract: parsedContract,
68
+ probes: parsedProbes,
69
+ runId,
70
+ });
71
+ // Sequential: the control legs are ordered by construction, and a parallel
72
+ // run would reset the fixture underneath another operation's witness.
73
+ const observations = [];
74
+ for (const leg of plan.legs) {
75
+ emit(sink, { runId, stage: 'preflight', message: plannedLine(leg.legId) });
76
+ observations.push(await invokePort({
77
+ request: leg.request,
78
+ requestParser: probeParsers.request,
79
+ responseParser: probeParsers.response,
80
+ port: port.probe,
81
+ signal,
82
+ requestPath: 'ProbeRequest',
83
+ responsePath: 'ProbeObservation',
84
+ }));
85
+ // `invokePort` either returns an observation or throws, so every leg
86
+ // reaching here was answered and the unobserved line is unreachable.
87
+ emit(sink, { runId, stage: 'preflight', message: observedLine(leg.legId) });
88
+ }
89
+ const verdict = parseVerdict(reducePreflight(plan, { observations }));
90
+ emit(sink, {
91
+ runId,
92
+ stage: 'preflight',
93
+ message: closingLine(plan.legs.length, verdict.passed),
94
+ });
95
+ return verdict;
96
+ }
97
+ /**
98
+ * The same pre-flight verdict for a caller who probed by some other means:
99
+ * plans, skips the awaiting, reduces over observations it is handed.
100
+ */
101
+ export function preflightFromObservations(options) {
102
+ const { runId, sink } = options;
103
+ const parsedContract = parseContract(options.contract);
104
+ const parsedProbes = parseProbes(options.probes);
105
+ const parsedObservations = parseObservations(options.observations);
106
+ const plan = planPreflight({
107
+ contract: parsedContract,
108
+ probes: parsedProbes,
109
+ runId,
110
+ });
111
+ // `reducePreflight` binds an observation to a leg by `probeId`, so the
112
+ // emitted lines are keyed the same way rather than by array position.
113
+ const answered = new Set(parsedObservations.map((observation) => observation.probeId));
114
+ for (const leg of plan.legs) {
115
+ emit(sink, { runId, stage: 'preflight', message: plannedLine(leg.legId) });
116
+ emit(sink, {
117
+ runId,
118
+ stage: 'preflight',
119
+ message: answered.has(leg.legId)
120
+ ? observedLine(leg.legId)
121
+ : `leg "${leg.legId}": no observation`,
122
+ });
123
+ }
124
+ const verdict = parseVerdict(reducePreflight(plan, { observations: parsedObservations }));
125
+ emit(sink, {
126
+ runId,
127
+ stage: 'preflight',
128
+ message: closingLine(plan.legs.length, verdict.passed),
129
+ });
130
+ return verdict;
131
+ }
@@ -0,0 +1,4 @@
1
+ import type { SealedEvaluatorBrief } from '../core/schemas/sealed-evaluator-brief.ts';
2
+ export declare function seal(input: unknown, options?: {
3
+ readonly strict?: boolean;
4
+ }): SealedEvaluatorBrief;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The synchronous application boundary for the seal stage. Compiles, then
3
+ * seals: `compile` and `seal` are one stage for a caller, and sealing an
4
+ * uncompiled contract emits a brief from declarations no discipline check has
5
+ * seen.
6
+ *
7
+ * Sequencing two pure core stages is not decision logic; `preflight.ts`
8
+ * already sequences plan and reduce. `StructuralFailure` and `RuntimeFault`
9
+ * propagate. `core/seal` throws `TypeError` on preconditions compilation does
10
+ * not cover, and this boundary converts those so an untyped throw never
11
+ * reaches a caller.
12
+ */
13
+ import { RuntimeFault } from '../core/schemas/faults.js';
14
+ import { seal as sealContract } from '../core/seal/seal.js';
15
+ import { compile } from './compile.js';
16
+ export function seal(input, options) {
17
+ const contract = compile(input, options);
18
+ try {
19
+ // `core/seal` validates the assembled brief and freezes it on the way
20
+ // out, so AD-28's outbound check is already done and is not repeated.
21
+ return sealContract(contract);
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof TypeError))
25
+ throw error;
26
+ throw new RuntimeFault('schema-parse-failure', 'EvalContract', `the contract compiles but cannot be sealed: ${error.message}`, { cause: error });
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * RFC 8785 canonical bytes plus a trailing newline, as text. Strip the newline
3
+ * and the remaining bytes are what `digestArtifact` hashes, so a caller can
4
+ * write a file and compute its digest from the same string.
5
+ */
6
+ export declare function serializeArtifact(artifact: unknown, artifactPath: string): string;
@@ -0,0 +1,9 @@
1
+ import { canonicalize } from '../core/canonical/canonicalize.js';
2
+ /**
3
+ * RFC 8785 canonical bytes plus a trailing newline, as text. Strip the newline
4
+ * and the remaining bytes are what `digestArtifact` hashes, so a caller can
5
+ * write a file and compute its digest from the same string.
6
+ */
7
+ export function serializeArtifact(artifact, artifactPath) {
8
+ return `${new TextDecoder().decode(canonicalize(artifact, artifactPath))}\n`;
9
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The command line, parsed. Pure and synchronous over an `argv` array: no
3
+ * filesystem, no `process`, no environment, no prompt and no terminal check,
4
+ * because every command is non-interactive. Paths are carried through exactly
5
+ * as given and resolved by the caller.
6
+ */
7
+ export type Command = 'compile' | 'seal' | 'preflight';
8
+ /** The four input flags. Closed, so a usage error can name the flag set. */
9
+ export type InputKey = 'in' | 'contract' | 'probes' | 'observations';
10
+ export type ParsedInvocation = {
11
+ readonly kind: 'help';
12
+ readonly command: Command | null;
13
+ } | {
14
+ readonly kind: 'version';
15
+ } | {
16
+ readonly kind: 'usage-error';
17
+ readonly message: string;
18
+ } | {
19
+ readonly kind: 'run';
20
+ readonly command: Command;
21
+ readonly inputs: Readonly<Partial<Record<InputKey, string>>>;
22
+ readonly out: string | null;
23
+ readonly runId: string | null;
24
+ readonly strictInputs: boolean;
25
+ readonly strict: boolean;
26
+ };
27
+ export declare const COMMANDS: readonly Command[];
28
+ export declare function parseArguments(argv: readonly string[]): ParsedInvocation;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The command line, parsed. Pure and synchronous over an `argv` array: no
3
+ * filesystem, no `process`, no environment, no prompt and no terminal check,
4
+ * because every command is non-interactive. Paths are carried through exactly
5
+ * as given and resolved by the caller.
6
+ */
7
+ export const COMMANDS = ['compile', 'seal', 'preflight'];
8
+ /** The input flags each command takes, and which of them it requires. */
9
+ const INPUT_KEYS = {
10
+ compile: ['in'],
11
+ seal: ['in'],
12
+ preflight: ['contract', 'probes', 'observations'],
13
+ };
14
+ /** `--strict-inputs` is AD-4's compile mode; `preflight` has no compile step. */
15
+ const TAKES_STRICT_INPUTS = {
16
+ compile: true,
17
+ seal: true,
18
+ preflight: false,
19
+ };
20
+ /** `--run-id` names the run a verdict is minted for, which only `preflight` does. */
21
+ const TAKES_RUN_ID = {
22
+ compile: false,
23
+ seal: false,
24
+ preflight: true,
25
+ };
26
+ const STDIN = '-';
27
+ const isCommand = (token) => COMMANDS.includes(token);
28
+ const usageError = (message) => ({
29
+ kind: 'usage-error',
30
+ message,
31
+ });
32
+ /** `--flag=value`, split on the first `=` so a value may contain one. */
33
+ function splitFlag(token) {
34
+ const equals = token.indexOf('=');
35
+ if (equals === -1)
36
+ return { flag: token, inline: null };
37
+ return { flag: token.slice(0, equals), inline: token.slice(equals + 1) };
38
+ }
39
+ export function parseArguments(argv) {
40
+ const first = argv[0];
41
+ if (first === undefined) {
42
+ return usageError(`no command given; expected one of ${COMMANDS.join(', ')}`);
43
+ }
44
+ if (first === '--help' || first === '-h' || first === 'help') {
45
+ const target = argv[1];
46
+ if (target === undefined)
47
+ return { kind: 'help', command: null };
48
+ if (isCommand(target))
49
+ return { kind: 'help', command: target };
50
+ return usageError(`unknown command "${target}"; expected one of ${COMMANDS.join(', ')}`);
51
+ }
52
+ if (first === '--version' || first === '-V')
53
+ return { kind: 'version' };
54
+ if (!isCommand(first)) {
55
+ return usageError(`unknown command "${first}"; expected one of ${COMMANDS.join(', ')}`);
56
+ }
57
+ return parseCommand(first, argv.slice(1));
58
+ }
59
+ function parseCommand(command, rest) {
60
+ const valueFlags = new Map();
61
+ for (const key of INPUT_KEYS[command])
62
+ valueFlags.set(`--${key}`, key);
63
+ valueFlags.set('--out', 'out');
64
+ if (TAKES_RUN_ID[command])
65
+ valueFlags.set('--run-id', 'run-id');
66
+ const inputs = {};
67
+ const seen = new Map();
68
+ let out = null;
69
+ let runId = null;
70
+ let strictInputs = true;
71
+ let strict = false;
72
+ const takeValue = (flag, inline, index) => {
73
+ if (inline !== null) {
74
+ if (inline === '')
75
+ return usageError(`${flag} was given an empty value`);
76
+ return { value: inline, next: index + 1 };
77
+ }
78
+ const value = rest[index + 1];
79
+ if (value === undefined)
80
+ return usageError(`${flag} requires a value`);
81
+ if (value === '')
82
+ return usageError(`${flag} was given an empty value`);
83
+ // A flag-shaped token is the next flag, so the space form treats it as a
84
+ // missing value. Bare `-` stays legal because it names stdin, and a path
85
+ // beginning with `-` is what the equals form is for.
86
+ if (value.length > 1 && value.startsWith('-')) {
87
+ return usageError(`${flag} requires a value, but the next token is "${value}"; use ${flag}=${value} for a value that begins with "-"`);
88
+ }
89
+ return { value, next: index + 2 };
90
+ };
91
+ for (let index = 0; index < rest.length;) {
92
+ const token = rest[index];
93
+ if (token === '--') {
94
+ const positional = rest[index + 1];
95
+ if (positional === undefined)
96
+ break;
97
+ return usageError(`${command} takes no positional argument, but got "${positional}"`);
98
+ }
99
+ if (token === '--help' || token === '-h') {
100
+ return { kind: 'help', command };
101
+ }
102
+ const { flag, inline } = splitFlag(token);
103
+ const target = valueFlags.get(flag);
104
+ if (target !== undefined) {
105
+ const taken = takeValue(flag, inline, index);
106
+ if ('kind' in taken)
107
+ return taken;
108
+ const previous = seen.get(flag);
109
+ if (previous !== undefined && previous !== taken.value) {
110
+ return usageError(`${flag} given twice with different values, "${previous}" and "${taken.value}"`);
111
+ }
112
+ seen.set(flag, taken.value);
113
+ if (target === 'out')
114
+ out = taken.value;
115
+ else if (target === 'run-id')
116
+ runId = taken.value;
117
+ else
118
+ inputs[target] = taken.value;
119
+ index = taken.next;
120
+ continue;
121
+ }
122
+ if (inline !== null) {
123
+ return usageError(`unknown flag "${flag}" in "${token}"`);
124
+ }
125
+ if (flag === '--strict') {
126
+ strict = true;
127
+ index += 1;
128
+ continue;
129
+ }
130
+ if (TAKES_STRICT_INPUTS[command] && flag === '--strict-inputs') {
131
+ strictInputs = true;
132
+ index += 1;
133
+ continue;
134
+ }
135
+ if (TAKES_STRICT_INPUTS[command] && flag === '--no-strict-inputs') {
136
+ strictInputs = false;
137
+ index += 1;
138
+ continue;
139
+ }
140
+ return usageError(`unknown flag "${token}" for ${command}`);
141
+ }
142
+ const missing = INPUT_KEYS[command]
143
+ .filter((key) => key !== 'in' && inputs[key] === undefined)
144
+ .map((key) => `--${key}`);
145
+ if (TAKES_RUN_ID[command] && runId === null)
146
+ missing.push('--run-id');
147
+ // `PreflightVerdict.runId` is `z.string().min(1)`, which a run of spaces
148
+ // satisfies. It then renders as blank in every diagnostic line and names
149
+ // nothing a reader can correlate, so the argument surface refuses it here
150
+ // rather than the schema refusing what it declares legal.
151
+ if (runId !== null && runId.trim() === '') {
152
+ return usageError('--run-id must contain a non-whitespace character');
153
+ }
154
+ if (missing.length > 0) {
155
+ return usageError(`${command} requires ${missing.join(', ')}`);
156
+ }
157
+ // One stdin cannot serve two readers, so at most one input may be `-`.
158
+ const fromStdin = INPUT_KEYS[command].filter((key) => inputs[key] === STDIN);
159
+ if (fromStdin.length > 1) {
160
+ const flags = fromStdin.map((key) => `--${key}`);
161
+ const named = flags.length === 2
162
+ ? `${flags.join(' and ')} both name`
163
+ : `${flags.join(', ')} all name`;
164
+ return usageError(`only one input may read stdin, but ${named} "-"`);
165
+ }
166
+ return { kind: 'run', command, inputs, out, runId, strictInputs, strict };
167
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * AD-21's exit codes, as a total function over what a command produced.
3
+ *
4
+ * Zero, one, and two are the verdict range. A command that produced no verdict
5
+ * never takes one or two, so a CI runner reading a two knows a verdict said
6
+ * FAIL and not that a compile failed. Zero stays plain success for every
7
+ * outcome, verdict or not.
8
+ */
9
+ import type { Verdict } from '../application/index.ts';
10
+ export declare const EXIT_OK = 0;
11
+ export declare const EXIT_CONCERNS_PROMOTED = 1;
12
+ export declare const EXIT_FAIL = 2;
13
+ export declare const EXIT_INVALID = 3;
14
+ export declare const EXIT_STRUCTURAL_FAILURE = 4;
15
+ export declare const EXIT_FAULT = 5;
16
+ /** sysexits.h EX_USAGE. Outside the verdict range and outside AD-21's codes. */
17
+ export declare const EXIT_USAGE = 64;
18
+ export type CommandOutcome = {
19
+ readonly kind: 'artifact';
20
+ } | {
21
+ readonly kind: 'preflight';
22
+ readonly passed: boolean;
23
+ } | {
24
+ readonly kind: 'verdict';
25
+ readonly verdict: Verdict;
26
+ readonly evidenceConditionsOnly: boolean;
27
+ } | {
28
+ readonly kind: 'structural-failure';
29
+ } | {
30
+ readonly kind: 'fault';
31
+ } | {
32
+ readonly kind: 'usage-error';
33
+ };
34
+ export declare function exitCodeFor(outcome: CommandOutcome, options: {
35
+ readonly strict: boolean;
36
+ }): number;
@@ -0,0 +1,39 @@
1
+ export const EXIT_OK = 0;
2
+ export const EXIT_CONCERNS_PROMOTED = 1;
3
+ export const EXIT_FAIL = 2;
4
+ export const EXIT_INVALID = 3;
5
+ export const EXIT_STRUCTURAL_FAILURE = 4;
6
+ export const EXIT_FAULT = 5;
7
+ /** sysexits.h EX_USAGE. Outside the verdict range and outside AD-21's codes. */
8
+ export const EXIT_USAGE = 64;
9
+ function verdictExit(outcome, strict) {
10
+ if (outcome.verdict === 'FAIL')
11
+ return EXIT_FAIL;
12
+ // `--strict` promotes CONCERNS to one, "except a CONCERNS whose only firing
13
+ // conditions are evidence conditions, which `--strict` never promotes"
14
+ // (AD-21). A thinner measurement is not a claim about the system.
15
+ if (outcome.verdict === 'CONCERNS' &&
16
+ strict &&
17
+ !outcome.evidenceConditionsOnly) {
18
+ return EXIT_CONCERNS_PROMOTED;
19
+ }
20
+ return EXIT_OK;
21
+ }
22
+ export function exitCodeFor(outcome, options) {
23
+ switch (outcome.kind) {
24
+ case 'artifact':
25
+ return EXIT_OK;
26
+ // A failed pre-flight invalidates the run (AD-10). Invalid is 3; a 2
27
+ // would read as a scored FAIL verdict.
28
+ case 'preflight':
29
+ return outcome.passed ? EXIT_OK : EXIT_INVALID;
30
+ case 'verdict':
31
+ return verdictExit(outcome, options.strict);
32
+ case 'structural-failure':
33
+ return EXIT_STRUCTURAL_FAILURE;
34
+ case 'fault':
35
+ return EXIT_FAULT;
36
+ case 'usage-error':
37
+ return EXIT_USAGE;
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: readonly string[]): Promise<void>;
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `bin` entry: the only file in the package that reads `process.argv`,
4
+ * reads stdin, or writes to a stream. It holds no command logic, so its whole
5
+ * body is building a `RunEnvironment` and turning an outcome into
6
+ * `process.exitCode`.
7
+ *
8
+ * `process.exit` is called nowhere. Exiting truncates a pending stdout write,
9
+ * and an artifact larger than a pipe buffer is the ordinary case here.
10
+ */
11
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import process from 'node:process';
14
+ import { parseArguments } from './arguments.js';
15
+ import { exitCodeFor } from './exit-codes.js';
16
+ import { renderError, renderUsage } from './render.js';
17
+ import { run } from './run.js';
18
+ /**
19
+ * A path the invocation named that the filesystem will not give us. The caller
20
+ * chose it, so it is a usage error and takes 64, which sits outside every code
21
+ * AD-21 assigns.
22
+ */
23
+ const PATH_ERROR_CODES = new Set([
24
+ 'ENOENT',
25
+ 'EACCES',
26
+ 'EISDIR',
27
+ 'ENOTDIR',
28
+ 'EPERM',
29
+ 'ELOOP',
30
+ 'ENAMETOOLONG',
31
+ ]);
32
+ function pathErrorOf(error) {
33
+ if (!(error instanceof Error))
34
+ return null;
35
+ const candidate = error;
36
+ if (candidate.code === undefined)
37
+ return null;
38
+ return PATH_ERROR_CODES.has(candidate.code) ? candidate : null;
39
+ }
40
+ /**
41
+ * The binary's own version. `cli/` may not import the root barrel, where
42
+ * `VERSION` lives, so the manifest is read beside the built file: `dist/cli/`
43
+ * and `src/cli/` are both two levels under the package root.
44
+ */
45
+ async function packageVersion() {
46
+ const manifestUrl = new URL('../../package.json', import.meta.url);
47
+ const manifest = JSON.parse(await readFile(manifestUrl, 'utf8'));
48
+ return manifest.version;
49
+ }
50
+ /** One read to end: a canonical JSON document has no incremental meaning. */
51
+ async function readStdin() {
52
+ const chunks = [];
53
+ for await (const chunk of process.stdin)
54
+ chunks.push(Buffer.from(chunk));
55
+ return Buffer.concat(chunks).toString('utf8');
56
+ }
57
+ function environmentOf(version) {
58
+ return {
59
+ readInput: async (source) => source === null ? await readStdin() : await readFile(source, 'utf8'),
60
+ writeArtifact: async (path, body) => {
61
+ // A named `--out` that does not exist yet is an ordinary way to
62
+ // invoke this, and failing with a raw ENOENT dressed as a usage
63
+ // error helps nobody. Creating an output directory mutates no
64
+ // input.
65
+ await mkdir(dirname(path), { recursive: true });
66
+ await writeFile(path, body, 'utf8');
67
+ },
68
+ writeOut: (body) => {
69
+ process.stdout.write(body);
70
+ },
71
+ writeDiagnostic: (line) => {
72
+ process.stderr.write(`${line}\n`);
73
+ },
74
+ // `resolve` on an already-absolute path still folds `.`, `..`, and a
75
+ // doubled separator, which is the whole point: returning an absolute
76
+ // path verbatim let `/d/./a.json` and `/d/a.json` name one file and pass
77
+ // the collision check, and the command then overwrote its own input.
78
+ resolvePath: (path) => resolve(path),
79
+ joinPath: (directory, name) => join(directory, name),
80
+ sameFile: async (left, right) => {
81
+ try {
82
+ const [a, b] = await Promise.all([stat(left), stat(right)]);
83
+ return a.dev === b.dev && a.ino === b.ino;
84
+ }
85
+ catch {
86
+ // One of them is not there, so they cannot be one file. The
87
+ // ordinary case: an output path that does not exist yet.
88
+ return false;
89
+ }
90
+ },
91
+ version,
92
+ };
93
+ }
94
+ /**
95
+ * Returns nothing and communicates through `process.exitCode`, so there is one
96
+ * exit mechanism. The top-level call below is what makes the file executable:
97
+ * without it the `bin` target loads and does nothing.
98
+ */
99
+ const writeStderr = (line) => {
100
+ process.stderr.write(`${line}\n`);
101
+ };
102
+ /**
103
+ * Every escape from `run` becomes an outcome, so every exit this binary takes
104
+ * is one `exitCodeFor` assigned. Letting one through would exit 1, and AD-21
105
+ * bars a command that produced no verdict from landing in the verdict range.
106
+ *
107
+ * `run` still rethrows a defect in our own code, and the stack still reaches
108
+ * stderr: what changes is the number beside it, which becomes the fault code
109
+ * rather than Node's default 1.
110
+ */
111
+ function failureOutcome(error) {
112
+ const pathError = pathErrorOf(error);
113
+ if (pathError !== null) {
114
+ writeStderr(renderUsage(`${pathError.syscall ?? 'open'} "${pathError.path ?? '(unknown path)'}": ${pathError.code}`));
115
+ return { kind: 'usage-error' };
116
+ }
117
+ const errno = error;
118
+ if (error instanceof Error && typeof errno.code === 'string') {
119
+ // An I/O failure outside the path allowlist: a full disk, a read-only
120
+ // mount, an exhausted descriptor table. The caller cannot fix it by
121
+ // changing the command line, so it is a fault and takes five.
122
+ writeStderr(renderError(error));
123
+ return { kind: 'fault' };
124
+ }
125
+ writeStderr(error instanceof Error ? (error.stack ?? error.message) : String(error));
126
+ return { kind: 'fault' };
127
+ }
128
+ export async function main(argv) {
129
+ try {
130
+ const invocation = parseArguments(argv);
131
+ const environment = environmentOf(await packageVersion());
132
+ const { outcome } = await run(invocation, environment);
133
+ process.exitCode = exitCodeFor(outcome, {
134
+ strict: invocation.kind === 'run' && invocation.strict,
135
+ });
136
+ }
137
+ catch (error) {
138
+ // `strict` promotes a CONCERNS and nothing else, and no failure outcome
139
+ // is a verdict, so the value here cannot change the code.
140
+ process.exitCode = exitCodeFor(failureOutcome(error), { strict: false });
141
+ }
142
+ }
143
+ await main(process.argv.slice(2));
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The four shapes the binary writes, and the exit-code table two documents
3
+ * share. Every line the CLI emits is produced here, so a format change is one
4
+ * file.
5
+ */
6
+ import { type Diagnostic } from '../application/index.ts';
7
+ /**
8
+ * Delegates to `serializeArtifact`; the canonical bytes are not re-derived
9
+ * here, so the text written to stdout is the text `digestArtifact` hashes.
10
+ */
11
+ export declare function renderArtifact(artifact: unknown, artifactPath: string): string;
12
+ /** `eval-quality: <stage>: <runId>: <message>` */
13
+ export declare function renderDiagnostic(diagnostic: Diagnostic): string;
14
+ /**
15
+ * `eval-quality: <code>: <artifactPath>: <detail>` for either error class.
16
+ * Anything else falls back to `String(error)`, which is what a defect in our
17
+ * own code looks like from outside.
18
+ */
19
+ export declare function renderError(error: unknown): string;
20
+ /**
21
+ * AD-21's seven exit codes, one line each. The `--help` output and the README
22
+ * table are this text, so the two cannot drift.
23
+ */
24
+ export declare const EXIT_CODE_TABLE = "Exit codes (AD-21):\n 0 success, and every verdict other than FAIL or a promoted CONCERNS\n 1 CONCERNS promoted by --strict\n 2 FAIL\n 3 invalid: a pre-flight verdict that did not pass\n 4 structural failure\n 5 runtime fault\n 64 usage error\n\n 1 and 2 report a scored verdict. Scoring ships in a later release, so no\n command here reaches either yet, and --strict changes no code this binary\n produces.";
25
+ /** `eval-quality: usage: <message>` */
26
+ export declare function renderUsage(message: string): string;