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.
- package/LICENSE +190 -0
- package/README.md +355 -0
- package/corpus/dev/README.md +36 -0
- package/corpus/dev/compile-seal-example/brief.json +1 -0
- package/corpus/dev/compile-seal-example/contract.json +1 -0
- package/corpus/dev/contracts/absent-collection-locations.json +1 -0
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
- package/corpus/dev/contracts/absent-success-indicator.json +1 -0
- package/corpus/dev/contracts/empty-channel-roles.json +1 -0
- package/corpus/dev/contracts/empty-collection-locations.json +1 -0
- package/corpus/dev/contracts/empty-request-shapes.json +1 -0
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
- package/corpus/dev/contracts/no-operation-inventory.json +1 -0
- package/corpus/dev/contracts/no-read-back-relation.json +1 -0
- package/corpus/dev/contracts/no-state-change-marker.json +1 -0
- package/corpus/dev/contracts/no-type-violating-step.json +1 -0
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
- package/corpus/dev/contracts/satisfied-declarations.json +1 -0
- package/corpus/dev/contracts/single-required-response-key.json +1 -0
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
- package/corpus/dev/index.json +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/local-corpus-adapter.d.ts +7 -0
- package/dist/adapters/local-corpus-adapter.js +78 -0
- package/dist/adapters/node-file-system-adapter.d.ts +7 -0
- package/dist/adapters/node-file-system-adapter.js +39 -0
- package/dist/adapters/port-boundary.d.ts +18 -0
- package/dist/adapters/port-boundary.js +83 -0
- package/dist/adapters/system-clock-adapter.d.ts +10 -0
- package/dist/adapters/system-clock-adapter.js +18 -0
- package/dist/application/compile.d.ts +4 -0
- package/dist/application/compile.js +24 -0
- package/dist/application/diagnostics.d.ts +22 -0
- package/dist/application/diagnostics.js +18 -0
- package/dist/application/index.d.ts +23 -0
- package/dist/application/index.js +17 -0
- package/dist/application/invoke-port.d.ts +2 -0
- package/dist/application/invoke-port.js +45 -0
- package/dist/application/preflight.d.ts +27 -0
- package/dist/application/preflight.js +131 -0
- package/dist/application/seal.d.ts +4 -0
- package/dist/application/seal.js +28 -0
- package/dist/application/serialize.d.ts +6 -0
- package/dist/application/serialize.js +9 -0
- package/dist/cli/arguments.d.ts +28 -0
- package/dist/cli/arguments.js +167 -0
- package/dist/cli/exit-codes.d.ts +36 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +143 -0
- package/dist/cli/render.d.ts +26 -0
- package/dist/cli/render.js +53 -0
- package/dist/cli/run.d.ts +41 -0
- package/dist/cli/run.js +203 -0
- package/dist/core/canonical/canonicalize.d.ts +1 -0
- package/dist/core/canonical/canonicalize.js +113 -0
- package/dist/core/canonical/digest.d.ts +7 -0
- package/dist/core/canonical/digest.js +81 -0
- package/dist/core/canonical/scan-json.d.ts +1 -0
- package/dist/core/canonical/scan-json.js +298 -0
- package/dist/core/canonical/value-domain.d.ts +4 -0
- package/dist/core/canonical/value-domain.js +113 -0
- package/dist/core/compile/compile.d.ts +24 -0
- package/dist/core/compile/compile.js +67 -0
- package/dist/core/compile/declarations.d.ts +5 -0
- package/dist/core/compile/declarations.js +22 -0
- package/dist/core/compile/expression-legality.d.ts +14 -0
- package/dist/core/compile/expression-legality.js +346 -0
- package/dist/core/compile/forbidden-inputs.d.ts +5 -0
- package/dist/core/compile/forbidden-inputs.js +19 -0
- package/dist/core/compile/interface-inventory.d.ts +10 -0
- package/dist/core/compile/interface-inventory.js +55 -0
- package/dist/core/compile/oracle-alignment.d.ts +14 -0
- package/dist/core/compile/oracle-alignment.js +93 -0
- package/dist/core/compile/reachability.d.ts +17 -0
- package/dist/core/compile/reachability.js +177 -0
- package/dist/core/compile/rubrics.d.ts +55 -0
- package/dist/core/compile/rubrics.js +198 -0
- package/dist/core/compile/scripting-bound.d.ts +5 -0
- package/dist/core/compile/scripting-bound.js +151 -0
- package/dist/core/compile/sensitivity-witness.d.ts +73 -0
- package/dist/core/compile/sensitivity-witness.js +232 -0
- package/dist/core/compile/waivers.d.ts +3 -0
- package/dist/core/compile/waivers.js +21 -0
- package/dist/core/coverage/coverage.d.ts +18 -0
- package/dist/core/coverage/coverage.js +47 -0
- package/dist/core/coverage/relevance.d.ts +75 -0
- package/dist/core/coverage/relevance.js +186 -0
- package/dist/core/coverage/rules.d.ts +7 -0
- package/dist/core/coverage/rules.js +18 -0
- package/dist/core/coverage/satisfaction.d.ts +107 -0
- package/dist/core/coverage/satisfaction.js +415 -0
- package/dist/core/coverage/table.d.ts +26 -0
- package/dist/core/coverage/table.js +222 -0
- package/dist/core/declared-inputs.d.ts +16 -0
- package/dist/core/declared-inputs.js +18 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
- package/dist/core/evaluate/evidence-resolution.js +153 -0
- package/dist/core/evaluate/operators.d.ts +87 -0
- package/dist/core/evaluate/operators.js +350 -0
- package/dist/core/evaluate/resolution.d.ts +40 -0
- package/dist/core/evaluate/resolution.js +336 -0
- package/dist/core/evaluate/resolved-value.d.ts +9 -0
- package/dist/core/evaluate/resolved-value.js +6 -0
- package/dist/core/failure-codes.d.ts +18 -0
- package/dist/core/failure-codes.js +50 -0
- package/dist/core/lineage/chain.d.ts +49 -0
- package/dist/core/lineage/chain.js +210 -0
- package/dist/core/lineage/freeze.d.ts +12 -0
- package/dist/core/lineage/freeze.js +35 -0
- package/dist/core/lineage/stage-table.d.ts +43 -0
- package/dist/core/lineage/stage-table.js +106 -0
- package/dist/core/preflight/plan.d.ts +67 -0
- package/dist/core/preflight/plan.js +313 -0
- package/dist/core/preflight/projection.d.ts +40 -0
- package/dist/core/preflight/projection.js +96 -0
- package/dist/core/preflight/reduce.d.ts +8 -0
- package/dist/core/preflight/reduce.js +180 -0
- package/dist/core/preflight/witness-evidence.d.ts +42 -0
- package/dist/core/preflight/witness-evidence.js +88 -0
- package/dist/core/probe/target-policy.d.ts +63 -0
- package/dist/core/probe/target-policy.js +328 -0
- package/dist/core/schemas/artifact-reference.d.ts +19 -0
- package/dist/core/schemas/artifact-reference.js +30 -0
- package/dist/core/schemas/artifact.d.ts +1153 -0
- package/dist/core/schemas/artifact.js +84 -0
- package/dist/core/schemas/constraint-ledger.d.ts +57 -0
- package/dist/core/schemas/constraint-ledger.js +143 -0
- package/dist/core/schemas/eval-contract.d.ts +402 -0
- package/dist/core/schemas/eval-contract.js +147 -0
- package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
- package/dist/core/schemas/evaluator-configuration.js +43 -0
- package/dist/core/schemas/evidence-artifact.d.ts +471 -0
- package/dist/core/schemas/evidence-artifact.js +232 -0
- package/dist/core/schemas/expression.d.ts +197 -0
- package/dist/core/schemas/expression.js +284 -0
- package/dist/core/schemas/faults.d.ts +9 -0
- package/dist/core/schemas/faults.js +32 -0
- package/dist/core/schemas/interface.d.ts +417 -0
- package/dist/core/schemas/interface.js +115 -0
- package/dist/core/schemas/isolation-manifest.d.ts +136 -0
- package/dist/core/schemas/isolation-manifest.js +92 -0
- package/dist/core/schemas/lineage.d.ts +21 -0
- package/dist/core/schemas/lineage.js +28 -0
- package/dist/core/schemas/oracle.d.ts +72 -0
- package/dist/core/schemas/oracle.js +35 -0
- package/dist/core/schemas/plan.d.ts +115 -0
- package/dist/core/schemas/plan.js +58 -0
- package/dist/core/schemas/pointer.d.ts +63 -0
- package/dist/core/schemas/pointer.js +94 -0
- package/dist/core/schemas/port-messages.d.ts +103 -0
- package/dist/core/schemas/port-messages.js +90 -0
- package/dist/core/schemas/preflight-verdict.d.ts +60 -0
- package/dist/core/schemas/preflight-verdict.js +38 -0
- package/dist/core/schemas/primitives.d.ts +78 -0
- package/dist/core/schemas/primitives.js +121 -0
- package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
- package/dist/core/schemas/private-artifact-manifest.js +46 -0
- package/dist/core/schemas/probe-body.d.ts +25 -0
- package/dist/core/schemas/probe-body.js +23 -0
- package/dist/core/schemas/probe-policy.d.ts +76 -0
- package/dist/core/schemas/probe-policy.js +33 -0
- package/dist/core/schemas/probe.d.ts +189 -0
- package/dist/core/schemas/probe.js +82 -0
- package/dist/core/schemas/publish.d.ts +30 -0
- package/dist/core/schemas/publish.js +135 -0
- package/dist/core/schemas/reference-set.d.ts +18 -0
- package/dist/core/schemas/reference-set.js +27 -0
- package/dist/core/schemas/rubric.d.ts +62 -0
- package/dist/core/schemas/rubric.js +68 -0
- package/dist/core/schemas/scoring-policy.d.ts +28 -0
- package/dist/core/schemas/scoring-policy.js +51 -0
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
- package/dist/core/schemas/sealed-run-record.d.ts +382 -0
- package/dist/core/schemas/sealed-run-record.js +215 -0
- package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
- package/dist/core/schemas/sensitivity-witness.js +86 -0
- package/dist/core/schemas/verdict.d.ts +25 -0
- package/dist/core/schemas/verdict.js +20 -0
- package/dist/core/schemas/waiver.d.ts +17 -0
- package/dist/core/schemas/waiver.js +24 -0
- package/dist/core/seal/derived-reference.d.ts +36 -0
- package/dist/core/seal/derived-reference.js +383 -0
- package/dist/core/seal/direction-prose.d.ts +13 -0
- package/dist/core/seal/direction-prose.js +120 -0
- package/dist/core/seal/plan-index.d.ts +63 -0
- package/dist/core/seal/plan-index.js +155 -0
- package/dist/core/seal/scripting-audit.d.ts +15 -0
- package/dist/core/seal/scripting-audit.js +51 -0
- package/dist/core/seal/seal.d.ts +18 -0
- package/dist/core/seal/seal.js +155 -0
- package/dist/core/stage-contracts.d.ts +30 -0
- package/dist/core/stage-contracts.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +22 -0
- package/dist/ports/clock-port.d.ts +13 -0
- package/dist/ports/clock-port.js +7 -0
- package/dist/ports/corpus-port.d.ts +16 -0
- package/dist/ports/corpus-port.js +7 -0
- package/dist/ports/environment-probe-port.d.ts +76 -0
- package/dist/ports/environment-probe-port.js +32 -0
- package/dist/ports/file-system-port.d.ts +27 -0
- package/dist/ports/file-system-port.js +11 -0
- package/dist/ports/port.d.ts +33 -0
- package/dist/ports/port.js +14 -0
- package/dist/testing/conformance.d.ts +88 -0
- package/dist/testing/conformance.js +281 -0
- package/dist/testing/index.d.ts +26 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/probe-conformance.d.ts +45 -0
- package/dist/testing/probe-conformance.js +186 -0
- package/package.json +130 -0
- package/schemas/artifact-reference.schema.json +64 -0
- package/schemas/eval-contract.schema.json +2145 -0
- package/schemas/evaluator-configuration.schema.json +199 -0
- package/schemas/evidence-artifact.schema.json +1229 -0
- package/schemas/isolation-manifest.schema.json +394 -0
- package/schemas/preflight-verdict.schema.json +114 -0
- package/schemas/private-artifact-manifest.schema.json +103 -0
- package/schemas/probe.schema.json +1151 -0
- package/schemas/rubric.schema.json +148 -0
- package/schemas/scoring-policy.schema.json +90 -0
- package/schemas/sealed-evaluator-brief.schema.json +268 -0
- 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>;
|
package/dist/cli/run.js
ADDED
|
@@ -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;
|