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,53 @@
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 { RuntimeFault, StructuralFailure, serializeArtifact, } from '../application/index.js';
7
+ const PREFIX = 'eval-quality';
8
+ /**
9
+ * Delegates to `serializeArtifact`; the canonical bytes are not re-derived
10
+ * here, so the text written to stdout is the text `digestArtifact` hashes.
11
+ */
12
+ export function renderArtifact(artifact, artifactPath) {
13
+ return serializeArtifact(artifact, artifactPath);
14
+ }
15
+ /** `eval-quality: <stage>: <runId>: <message>` */
16
+ export function renderDiagnostic(diagnostic) {
17
+ return `${PREFIX}: ${diagnostic.stage}: ${diagnostic.runId}: ${diagnostic.message}`;
18
+ }
19
+ /**
20
+ * `eval-quality: <code>: <artifactPath>: <detail>` for either error class.
21
+ * Anything else falls back to `String(error)`, which is what a defect in our
22
+ * own code looks like from outside.
23
+ */
24
+ export function renderError(error) {
25
+ if (error instanceof StructuralFailure || error instanceof RuntimeFault) {
26
+ const prefix = `${error.code} in ${error.artifactPath}: `;
27
+ const detail = error.message.startsWith(prefix)
28
+ ? error.message.slice(prefix.length)
29
+ : error.message;
30
+ return `${PREFIX}: ${error.code}: ${error.artifactPath}: ${detail}`;
31
+ }
32
+ return `${PREFIX}: ${String(error)}`;
33
+ }
34
+ /**
35
+ * AD-21's seven exit codes, one line each. The `--help` output and the README
36
+ * table are this text, so the two cannot drift.
37
+ */
38
+ export const EXIT_CODE_TABLE = `Exit codes (AD-21):
39
+ 0 success, and every verdict other than FAIL or a promoted CONCERNS
40
+ 1 CONCERNS promoted by --strict
41
+ 2 FAIL
42
+ 3 invalid: a pre-flight verdict that did not pass
43
+ 4 structural failure
44
+ 5 runtime fault
45
+ 64 usage error
46
+
47
+ 1 and 2 report a scored verdict. Scoring ships in a later release, so no
48
+ command here reaches either yet, and --strict changes no code this binary
49
+ produces.`;
50
+ /** `eval-quality: usage: <message>` */
51
+ export function renderUsage(message) {
52
+ return `${PREFIX}: usage: ${message}`;
53
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The three commands. Each one reads its inputs, makes exactly one call into
3
+ * `application/`, serializes what came back, and returns the outcome the exit
4
+ * code is derived from. Every effect is a member of `RunEnvironment`, so this
5
+ * module touches no stream, no `process`, and no Node builtin, and its tests
6
+ * run in memory.
7
+ */
8
+ import { compile, preflightFromObservations, seal } from '../application/index.ts';
9
+ import type { Command, ParsedInvocation } from './arguments.ts';
10
+ import type { CommandOutcome } from './exit-codes.ts';
11
+ export type RunEnvironment = {
12
+ readonly readInput: (source: string | null) => Promise<string>;
13
+ readonly writeArtifact: (path: string, body: string) => Promise<void>;
14
+ readonly writeOut: (body: string) => void;
15
+ readonly writeDiagnostic: (line: string) => void;
16
+ readonly resolvePath: (path: string) => string;
17
+ readonly joinPath: (directory: string, name: string) => string;
18
+ /**
19
+ * Whether two paths name one file on disk. String normalization cannot
20
+ * settle it: a symlink and a case-insensitive filesystem both alias two
21
+ * spellings that no amount of folding brings together.
22
+ */
23
+ readonly sameFile: (left: string, right: string) => Promise<boolean>;
24
+ readonly version: string;
25
+ };
26
+ /**
27
+ * The three orchestration calls, behind one object so a test can count them.
28
+ * An input/output count says nothing about how many calls into `application/`
29
+ * happened, which is the property AD-14 constrains.
30
+ */
31
+ export type ApplicationFacade = {
32
+ readonly compile: typeof compile;
33
+ readonly seal: typeof seal;
34
+ readonly preflightFromObservations: typeof preflightFromObservations;
35
+ };
36
+ export declare const APPLICATION: ApplicationFacade;
37
+ export type RunResult = {
38
+ readonly outcome: CommandOutcome;
39
+ };
40
+ export declare function helpText(command: Command | null): string;
41
+ export declare function run(invocation: ParsedInvocation, environment: RunEnvironment, application?: ApplicationFacade): Promise<RunResult>;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The three commands. Each one reads its inputs, makes exactly one call into
3
+ * `application/`, serializes what came back, and returns the outcome the exit
4
+ * code is derived from. Every effect is a member of `RunEnvironment`, so this
5
+ * module touches no stream, no `process`, and no Node builtin, and its tests
6
+ * run in memory.
7
+ */
8
+ import { compile, preflightFromObservations, RuntimeFault, StructuralFailure, seal, } from '../application/index.js';
9
+ import { EXIT_CODE_TABLE, renderArtifact, renderDiagnostic, renderError, renderUsage, } from './render.js';
10
+ export const APPLICATION = {
11
+ compile,
12
+ seal,
13
+ preflightFromObservations,
14
+ };
15
+ /** The artifact each command emits: its schema name and its file name. */
16
+ const EMITTED = {
17
+ compile: { artifactPath: 'EvalContract', kind: 'eval-contract' },
18
+ seal: {
19
+ artifactPath: 'SealedEvaluatorBrief',
20
+ kind: 'sealed-evaluator-brief',
21
+ },
22
+ preflight: { artifactPath: 'PreflightVerdict', kind: 'preflight-verdict' },
23
+ };
24
+ /** The schema an input key deserializes into, for the parse fault's path. */
25
+ const INPUT_ARTIFACT_PATH = {
26
+ in: 'EvalContract',
27
+ contract: 'EvalContract',
28
+ probes: 'Probe',
29
+ observations: 'ProbeObservation',
30
+ };
31
+ const USAGE = `Usage:
32
+ eval-quality compile [--in <path>] [--out <target>]
33
+ [--strict-inputs | --no-strict-inputs] [--strict]
34
+ eval-quality seal [--in <path>] [--out <target>]
35
+ [--strict-inputs | --no-strict-inputs] [--strict]
36
+ eval-quality preflight --contract <path> --probes <path> --observations <path>
37
+ --run-id <id> [--out <target>] [--strict]
38
+ eval-quality --help | -h | help [<command>]
39
+ eval-quality --version | -V`;
40
+ const COMMAND_USAGE = {
41
+ compile: `Usage:
42
+ eval-quality compile [--in <path>] [--out <target>]
43
+ [--strict-inputs | --no-strict-inputs] [--strict]
44
+
45
+ --in <path> the contract to compile; stdin when absent or "-"
46
+ --out <target> a .json file path, or a directory taking eval-contract.json
47
+ --strict-inputs reject undeclared inputs (default)
48
+ --no-strict-inputs allow undeclared inputs
49
+ --strict promote CONCERNS to exit 1`,
50
+ seal: `Usage:
51
+ eval-quality seal [--in <path>] [--out <target>]
52
+ [--strict-inputs | --no-strict-inputs] [--strict]
53
+
54
+ --in <path> the contract to compile and seal; stdin when absent or "-"
55
+ --out <target> a .json file path, or a directory taking sealed-evaluator-brief.json
56
+ --strict-inputs reject undeclared inputs (default)
57
+ --no-strict-inputs allow undeclared inputs
58
+ --strict promote CONCERNS to exit 1`,
59
+ preflight: `Usage:
60
+ eval-quality preflight --contract <path> --probes <path> --observations <path>
61
+ --run-id <id> [--out <target>] [--strict]
62
+
63
+ --contract <path> the compiled contract the plan is built from
64
+ --probes <path> the probe list the plan is built from
65
+ --observations <path> the observations to reduce over
66
+ --run-id <id> the run identifier the verdict is minted for
67
+ --out <target> a .json file path, or a directory taking preflight-verdict.json
68
+ --strict promote CONCERNS to exit 1`,
69
+ };
70
+ const IO_RULES = `Inputs and outputs:
71
+ An input flag left out reads stdin, and "-" names stdin explicitly; at most
72
+ one input may be "-". Without --out the artifact goes to stdout. An --out
73
+ ending in .json is a file path; anything else is a directory taking
74
+ <target>/<kind>.json. Diagnostics and errors go to stderr.`;
75
+ export function helpText(command) {
76
+ if (command === null) {
77
+ return `${USAGE}\n\n${IO_RULES}\n\n${EXIT_CODE_TABLE}`;
78
+ }
79
+ return `${COMMAND_USAGE[command]}\n\n${EXIT_CODE_TABLE}`;
80
+ }
81
+ /** `undefined` and `"-"` both mean stdin, which `readInput` takes as `null`. */
82
+ const sourceOf = (value) => value === undefined || value === '-' ? null : value;
83
+ async function readJson(environment, key, value) {
84
+ const text = await environment.readInput(sourceOf(value));
85
+ try {
86
+ return JSON.parse(text);
87
+ }
88
+ catch (error) {
89
+ // AD-28's `schema-parse-failure` covers an artifact that does not parse,
90
+ // and the CLI is the boundary that deserializes.
91
+ throw new RuntimeFault('schema-parse-failure', INPUT_ARTIFACT_PATH[key], `--${key} is not JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
92
+ }
93
+ }
94
+ /** The file `--out` names, or `null` when the artifact goes to stdout. */
95
+ function outputPath(environment, out, kind) {
96
+ if (out === null)
97
+ return null;
98
+ // The suffix is the whole classifier: the CLI never stats to decide this.
99
+ // Matched case-insensitively, because a case-insensitive filesystem accepts
100
+ // `run.JSON` for the same file and would otherwise be read as a directory.
101
+ return out.toLowerCase().endsWith('.json')
102
+ ? out
103
+ : environment.joinPath(out, `${kind}.json`);
104
+ }
105
+ /**
106
+ * An input is never mutated in place. Two checks, because neither alone is
107
+ * enough: comparing resolved paths catches `.`, `..`, and a doubled separator,
108
+ * and `sameFile` catches a symlink and a case-insensitive filesystem, which no
109
+ * string normalization can fold together.
110
+ */
111
+ async function collides(environment, target, inputs) {
112
+ const resolvedTarget = environment.resolvePath(target);
113
+ for (const [key, value] of Object.entries(inputs)) {
114
+ if (value === undefined || value === '-')
115
+ continue;
116
+ const resolvedInput = environment.resolvePath(value);
117
+ if (resolvedInput === resolvedTarget ||
118
+ (await environment.sameFile(resolvedInput, resolvedTarget))) {
119
+ return `--out resolves to "${resolvedTarget}", which is also --${key} "${value}"`;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ async function emitArtifact(environment, artifact, command, target) {
125
+ const body = renderArtifact(artifact, EMITTED[command].artifactPath);
126
+ if (target === null) {
127
+ environment.writeOut(body);
128
+ return;
129
+ }
130
+ await environment.writeArtifact(target, body);
131
+ }
132
+ export async function run(invocation, environment, application = APPLICATION) {
133
+ switch (invocation.kind) {
134
+ case 'help':
135
+ environment.writeOut(`${helpText(invocation.command)}\n`);
136
+ return { outcome: { kind: 'artifact' } };
137
+ case 'version':
138
+ environment.writeOut(`${environment.version}\n`);
139
+ return { outcome: { kind: 'artifact' } };
140
+ case 'usage-error':
141
+ environment.writeDiagnostic(renderUsage(invocation.message));
142
+ return { outcome: { kind: 'usage-error' } };
143
+ case 'run':
144
+ return await runCommand(invocation, environment, application);
145
+ }
146
+ }
147
+ async function runCommand(invocation, environment, application) {
148
+ const { command, inputs, out, strictInputs } = invocation;
149
+ const target = outputPath(environment, out, EMITTED[command].kind);
150
+ if (target !== null) {
151
+ const collision = await collides(environment, target, inputs);
152
+ if (collision !== null) {
153
+ environment.writeDiagnostic(renderUsage(collision));
154
+ return { outcome: { kind: 'usage-error' } };
155
+ }
156
+ }
157
+ try {
158
+ if (command === 'preflight') {
159
+ const verdict = await runPreflightCommand(invocation, environment, application, target);
160
+ return { outcome: { kind: 'preflight', passed: verdict.passed } };
161
+ }
162
+ const input = await readJson(environment, 'in', inputs.in);
163
+ const options = { strict: strictInputs };
164
+ const artifact = command === 'compile'
165
+ ? application.compile(input, options)
166
+ : application.seal(input, options);
167
+ // No diagnostic on success: neither command carries a run identifier,
168
+ // so any line either wrote would name no run and no stage.
169
+ await emitArtifact(environment, artifact, command, target);
170
+ return { outcome: { kind: 'artifact' } };
171
+ }
172
+ catch (error) {
173
+ if (error instanceof StructuralFailure) {
174
+ environment.writeDiagnostic(renderError(error));
175
+ return { outcome: { kind: 'structural-failure' } };
176
+ }
177
+ if (error instanceof RuntimeFault) {
178
+ environment.writeDiagnostic(renderError(error));
179
+ return { outcome: { kind: 'fault' } };
180
+ }
181
+ // A defect in our own code surfaces as a stack, never as exit 5.
182
+ throw error;
183
+ }
184
+ }
185
+ async function runPreflightCommand(invocation, environment, application, target) {
186
+ const { inputs, runId } = invocation;
187
+ const contract = (await readJson(environment, 'contract', inputs.contract));
188
+ const probes = (await readJson(environment, 'probes', inputs.probes));
189
+ const observations = (await readJson(environment, 'observations', inputs.observations));
190
+ const sink = (diagnostic) => {
191
+ environment.writeDiagnostic(renderDiagnostic(diagnostic));
192
+ };
193
+ const verdict = application.preflightFromObservations({
194
+ contract,
195
+ probes,
196
+ // The parser requires `--run-id` on this command, so it is never null.
197
+ runId: runId ?? '',
198
+ observations,
199
+ sink,
200
+ });
201
+ await emitArtifact(environment, verdict, 'preflight', target);
202
+ return verdict;
203
+ }
@@ -0,0 +1 @@
1
+ export declare function canonicalize(value: unknown, artifactPath: string): Uint8Array;
@@ -0,0 +1,113 @@
1
+ /** JSON to bytes */
2
+ import { RuntimeFault } from '../schemas/faults.js';
3
+ import { assertDomainNumber, assertDomainString, MAX_NESTING_DEPTH, } from './value-domain.js';
4
+ // RFC 8785 (JCS) canonical serialization, written in-house per AD-27; native
5
+ // JSON.stringify on individual numbers already implements JCS's number
6
+ // algorithm, so no hand-rolled Ryū is needed.
7
+ //
8
+ // Validation is fused into serialization as one traversal over a single
9
+ // ownKeys/descriptor snapshot, so exactly what was validated is what gets
10
+ // emitted. A second traversal would reopen the TOCTOU channel: an accessor or
11
+ // lying Proxy could answer the emit-time read differently than the
12
+ // validation read.
13
+ export function canonicalize(value, artifactPath) {
14
+ return new TextEncoder().encode(serialize(value, artifactPath, '$', new Set(), 0));
15
+ }
16
+ const fault = (artifactPath, detail) => {
17
+ throw new RuntimeFault('non-canonicalizable-value', artifactPath, detail);
18
+ };
19
+ function serialize(value, artifactPath, location, ancestors, depth) {
20
+ if (value === null)
21
+ return 'null';
22
+ switch (typeof value) {
23
+ case 'boolean':
24
+ return value ? 'true' : 'false';
25
+ case 'number':
26
+ assertDomainNumber(value, artifactPath, location);
27
+ // Whole-value JSON.stringify would drop undefined properties, honour
28
+ // toJSON, and skip key sorting, so this runs per scalar instead.
29
+ return JSON.stringify(value);
30
+ case 'string':
31
+ assertDomainString(value, artifactPath, location);
32
+ return JSON.stringify(value);
33
+ case 'object':
34
+ break;
35
+ default:
36
+ // undefined, function, bigint, symbol: unrepresentable, never coerced silently.
37
+ fault(artifactPath, `${typeof value} is not a hashed-artifact value at ${location}`);
38
+ }
39
+ const object = value;
40
+ if (depth >= MAX_NESTING_DEPTH) {
41
+ fault(artifactPath, `nesting depth exceeds ${MAX_NESTING_DEPTH} at ${location}`);
42
+ }
43
+ if (ancestors.has(object)) {
44
+ fault(artifactPath, `cyclic reference at ${location}`);
45
+ }
46
+ ancestors.add(object);
47
+ let rendered;
48
+ if (Array.isArray(object)) {
49
+ if (Object.getPrototypeOf(object) !== Array.prototype) {
50
+ fault(artifactPath, `non-plain array at ${location}`);
51
+ }
52
+ // One property-table read; elements come from descriptor values, never a
53
+ // second [[Get]]. A toJSON carrier surfaces as a non-index key (own);
54
+ // the prototype check rules out an inherited one.
55
+ const descriptors = Object.getOwnPropertyDescriptors(object);
56
+ if (Object.getOwnPropertySymbols(descriptors).length > 0) {
57
+ fault(artifactPath, `symbol-keyed property at ${location}`);
58
+ }
59
+ const elements = [];
60
+ for (const [key, descriptor] of Object.entries(descriptors)) {
61
+ if (key === 'length')
62
+ continue;
63
+ if (!('value' in descriptor)) {
64
+ fault(artifactPath, `accessor property ${JSON.stringify(key)} at ${location}`);
65
+ }
66
+ if (!descriptor.enumerable) {
67
+ fault(artifactPath, `non-enumerable property ${JSON.stringify(key)} at ${location}`);
68
+ }
69
+ if (key !== String(elements.length)) {
70
+ fault(artifactPath, `array with holes or non-index properties at ${location}`);
71
+ }
72
+ elements.push(descriptor.value);
73
+ }
74
+ if (elements.length !== object.length) {
75
+ fault(artifactPath, `array with holes or non-index properties at ${location}`);
76
+ }
77
+ rendered = `[${elements
78
+ .map((element, index) => serialize(element, artifactPath, `${location}[${index}]`, ancestors, depth + 1))
79
+ .join(',')}]`;
80
+ }
81
+ else {
82
+ const prototype = Object.getPrototypeOf(object);
83
+ if (prototype !== Object.prototype && prototype !== null) {
84
+ fault(artifactPath, `non-plain object at ${location}`);
85
+ }
86
+ const descriptors = Object.getOwnPropertyDescriptors(object);
87
+ if (Object.getOwnPropertySymbols(descriptors).length > 0) {
88
+ fault(artifactPath, `symbol-keyed property at ${location}`);
89
+ }
90
+ const properties = [];
91
+ for (const [key, descriptor] of Object.entries(descriptors)) {
92
+ if (!('value' in descriptor)) {
93
+ fault(artifactPath, `accessor property ${JSON.stringify(key)} at ${location}`);
94
+ }
95
+ if (!descriptor.enumerable) {
96
+ fault(artifactPath, `non-enumerable property ${JSON.stringify(key)} at ${location}`);
97
+ }
98
+ // Keys are validated on the same snapshot they are emitted from.
99
+ assertDomainString(key, artifactPath, `${location} (object key)`);
100
+ // A callable toJSON is rejected wherever it sits: as an own data
101
+ // property its function value faults in the recursion below.
102
+ properties.push([key, descriptor.value]);
103
+ }
104
+ // Plain < on JS strings compares UTF-16 code units, the required order:
105
+ // never localeCompare, never Intl, never .normalize().
106
+ properties.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
107
+ rendered = `{${properties
108
+ .map(([key, entry]) => `${JSON.stringify(key)}:${serialize(entry, artifactPath, `${location}.${key}`, ancestors, depth + 1)}`)
109
+ .join(',')}}`;
110
+ }
111
+ ancestors.delete(object);
112
+ return rendered;
113
+ }
@@ -0,0 +1,7 @@
1
+ export declare const COMPOSITE_PROTOCOL_TAG = "eval-quality/composite/v1";
2
+ export declare const DIRECTORY_PROTOCOL_TAG = "eval-quality/directory/v1";
3
+ export declare function digestArtifact(value: unknown, artifactPath: string): string;
4
+ export declare function digestBytes(bytes: Uint8Array): string;
5
+ export declare function digestJson(input: Uint8Array | string, artifactPath: string): string;
6
+ export declare function digestComposite(fields: Record<string, unknown>, artifactPath: string): string;
7
+ export declare function digestDirectory(members: Record<string, string>, artifactPath: string): string;
@@ -0,0 +1,81 @@
1
+ /** the five digest functions. */
2
+ import { createHash } from 'node:crypto';
3
+ import { DIGEST_FORM } from '../schemas/primitives.js';
4
+ import { canonicalize } from './canonicalize.js';
5
+ import { scanJson } from './scan-json.js';
6
+ // AD-27 digest computation. node:crypto is the one permitted builtin in core/
7
+ // (AD-1: digesting is deterministic; there is deliberately no digest port).
8
+ // No artifact carries its own digest: digests live only in referring
9
+ // artifacts, so there is no self-exclusion rule here.
10
+ export const COMPOSITE_PROTOCOL_TAG = 'eval-quality/composite/v1';
11
+ export const DIRECTORY_PROTOCOL_TAG = 'eval-quality/directory/v1';
12
+ const render = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
13
+ export function digestArtifact(value, artifactPath) {
14
+ return render(canonicalize(value, artifactPath));
15
+ }
16
+ export function digestBytes(bytes) {
17
+ return render(bytes);
18
+ }
19
+ // Entry point for raw hashed-artifact text or bytes: scans lexically first
20
+ // (see scan-json.ts for why), then digests. Callers holding raw input use
21
+ // this, never digestArtifact(JSON.parse(text)).
22
+ export function digestJson(input, artifactPath) {
23
+ return digestArtifact(scanJson(input, artifactPath), artifactPath);
24
+ }
25
+ // A composite digest is a digest over a domain-separated tagged object with
26
+ // named fields, never a concatenation of member strings.
27
+ export function digestComposite(fields, artifactPath) {
28
+ if (Object.keys(fields).length === 0) {
29
+ // Empty composites are rejected: relaxing this later can't break frozen
30
+ // digests, but legalizing it now would be irreversible.
31
+ throw new TypeError('composite requires at least one field');
32
+ }
33
+ if (Object.hasOwn(fields, 'protocol')) {
34
+ throw new TypeError('composite fields must not carry a "protocol" member');
35
+ }
36
+ // The spread snapshots each field once into a fresh plain object, so the
37
+ // canonicalizer digests exactly what was read here.
38
+ return digestArtifact({ protocol: COMPOSITE_PROTOCOL_TAG, ...fields }, artifactPath);
39
+ }
40
+ // Member paths must arrive in one canonical spelling: relative, forward-slash,
41
+ // no empty/dot/dot-dot segments. Two producers naming the same file "a" and
42
+ // "./a" would otherwise mint different digests with no fault raised. Unicode
43
+ // normalization is deliberately not applied (the canonicalizer is never
44
+ // normalization-aware); NFC vs NFD spelling is the producer's responsibility
45
+ // per the fixtures README.
46
+ function assertMemberPath(path) {
47
+ if (path === '') {
48
+ throw new TypeError('directory member path must not be empty');
49
+ }
50
+ if (path.includes('\\')) {
51
+ throw new TypeError(`directory member path must use forward slashes: ${JSON.stringify(path)}`);
52
+ }
53
+ if (path.startsWith('/')) {
54
+ throw new TypeError(`directory member path must be relative: ${JSON.stringify(path)}`);
55
+ }
56
+ for (const segment of path.split('/')) {
57
+ if (segment === '' || segment === '.' || segment === '..') {
58
+ throw new TypeError(`directory member path not in canonical form: ${JSON.stringify(path)}`);
59
+ }
60
+ }
61
+ }
62
+ // A directory digest is a composite over its members ordered by path. Members
63
+ // nest under a fixed "members" field so a path can never collide with the
64
+ // protocol tag; canonical key sorting orders them by path automatically:
65
+ // UTF-16 code-unit order, not the UTF-8 byte order git produces.
66
+ export function digestDirectory(members, artifactPath) {
67
+ const entries = Object.entries(members);
68
+ if (entries.length === 0) {
69
+ throw new TypeError('directory requires at least one member');
70
+ }
71
+ for (const [path, digest] of entries) {
72
+ assertMemberPath(path);
73
+ if (!DIGEST_FORM.test(digest)) {
74
+ throw new TypeError(`directory member ${JSON.stringify(path)} is not a sha256: digest`);
75
+ }
76
+ }
77
+ // Digest the snapshot read above, not the caller's object: a getter or
78
+ // Proxy could otherwise answer the canonicalizer's read with a value the
79
+ // DIGEST_FORM check never saw.
80
+ return digestArtifact({ protocol: DIRECTORY_PROTOCOL_TAG, members: Object.fromEntries(entries) }, artifactPath);
81
+ }
@@ -0,0 +1 @@
1
+ export declare function scanJson(input: Uint8Array | string, artifactPath: string): unknown;