eval-quality 0.1.0 → 0.3.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/README.md +142 -67
- package/corpus/dev/README.md +20 -13
- package/corpus/dev/compile-seal-example/brief.json +1 -1
- package/corpus/dev/compile-seal-example/contract.json +1 -1
- package/corpus/dev/contracts/absent-collection-locations.json +1 -1
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
- package/corpus/dev/contracts/absent-success-indicator.json +1 -1
- package/corpus/dev/contracts/empty-channel-roles.json +1 -1
- package/corpus/dev/contracts/empty-collection-locations.json +1 -1
- package/corpus/dev/contracts/empty-request-shapes.json +1 -1
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
- package/corpus/dev/contracts/fragment-selection.json +1 -0
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
- package/corpus/dev/contracts/no-operation-inventory.json +1 -1
- package/corpus/dev/contracts/no-read-back-relation.json +1 -1
- package/corpus/dev/contracts/no-state-change-marker.json +1 -1
- package/corpus/dev/contracts/no-type-violating-step.json +1 -1
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/review-corpus.json +1 -0
- package/corpus/dev/contracts/satisfied-declarations.json +1 -1
- package/corpus/dev/contracts/single-required-response-key.json +1 -1
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
- package/corpus/dev/index.json +1 -1
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +1 -0
- package/dist/application/score.d.ts +48 -0
- package/dist/application/score.js +186 -0
- package/dist/cli/arguments.d.ts +7 -3
- package/dist/cli/arguments.js +73 -5
- package/dist/cli/exit-codes.d.ts +6 -2
- package/dist/cli/exit-codes.js +8 -9
- package/dist/cli/main.js +9 -0
- package/dist/cli/render.d.ts +3 -6
- package/dist/cli/render.js +57 -5
- package/dist/cli/run.d.ts +13 -3
- package/dist/cli/run.js +136 -16
- package/dist/core/canonical/canonicalize.d.ts +12 -0
- package/dist/core/canonical/canonicalize.js +18 -4
- package/dist/core/compile/bindings.d.ts +89 -0
- package/dist/core/compile/bindings.js +334 -0
- package/dist/core/compile/compile.d.ts +17 -0
- package/dist/core/compile/compile.js +37 -1
- package/dist/core/compile/excluded-content.d.ts +11 -0
- package/dist/core/compile/excluded-content.js +42 -0
- package/dist/core/compile/expression-legality.d.ts +49 -0
- package/dist/core/compile/expression-legality.js +158 -38
- package/dist/core/compile/forbidden-inputs.d.ts +8 -1
- package/dist/core/compile/forbidden-inputs.js +16 -3
- package/dist/core/compile/interface-inventory.d.ts +57 -2
- package/dist/core/compile/interface-inventory.js +119 -13
- package/dist/core/compile/reachability.d.ts +28 -1
- package/dist/core/compile/reachability.js +167 -35
- package/dist/core/compile/schema-version.d.ts +2 -0
- package/dist/core/compile/schema-version.js +25 -0
- package/dist/core/compile/sensitivity-witness.d.ts +31 -12
- package/dist/core/compile/sensitivity-witness.js +110 -23
- package/dist/core/compile/step-reference.d.ts +2 -0
- package/dist/core/compile/step-reference.js +49 -0
- package/dist/core/coverage/operations.d.ts +62 -0
- package/dist/core/coverage/operations.js +57 -0
- package/dist/core/coverage/relevance.d.ts +4 -2
- package/dist/core/coverage/relevance.js +22 -23
- package/dist/core/coverage/satisfaction.d.ts +2 -2
- package/dist/core/coverage/satisfaction.js +73 -48
- package/dist/core/declared-inputs.d.ts +83 -4
- package/dist/core/declared-inputs.js +105 -8
- package/dist/core/emit/emit.d.ts +9 -0
- package/dist/core/emit/emit.js +214 -0
- package/dist/core/emit/private-artifact-digest.d.ts +11 -0
- package/dist/core/emit/private-artifact-digest.js +31 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +10 -3
- package/dist/core/evaluate/evidence-resolution.js +74 -11
- package/dist/core/evaluate/operators.d.ts +22 -0
- package/dist/core/evaluate/operators.js +17 -1
- package/dist/core/evaluate/resolution.d.ts +14 -3
- package/dist/core/evaluate/resolution.js +63 -3
- package/dist/core/excluded-content.d.ts +65 -0
- package/dist/core/excluded-content.js +113 -0
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +7 -2
- package/dist/core/ingest/conditions.d.ts +201 -0
- package/dist/core/ingest/conditions.js +48 -0
- package/dist/core/ingest/index.d.ts +13 -0
- package/dist/core/ingest/index.js +11 -0
- package/dist/core/ingest/ingest.d.ts +68 -0
- package/dist/core/ingest/ingest.js +318 -0
- package/dist/core/lineage/stage-table.d.ts +21 -4
- package/dist/core/lineage/stage-table.js +35 -3
- package/dist/core/preflight/plan.d.ts +7 -5
- package/dist/core/preflight/plan.js +75 -32
- package/dist/core/preflight/projection.d.ts +6 -3
- package/dist/core/preflight/projection.js +22 -2
- package/dist/core/preflight/reduce.js +47 -7
- package/dist/core/preflight/witness-evidence.d.ts +5 -5
- package/dist/core/preflight/witness-evidence.js +73 -20
- package/dist/core/schemas/artifact.d.ts +1049 -30
- package/dist/core/schemas/constraint-ledger.js +51 -0
- package/dist/core/schemas/defect-signature.d.ts +695 -0
- package/dist/core/schemas/defect-signature.js +175 -0
- package/dist/core/schemas/eval-contract.d.ts +550 -12
- package/dist/core/schemas/eval-contract.js +32 -2
- package/dist/core/schemas/evidence-artifact.d.ts +109 -19
- package/dist/core/schemas/evidence-artifact.js +64 -8
- package/dist/core/schemas/interface.d.ts +741 -13
- package/dist/core/schemas/interface.js +120 -7
- package/dist/core/schemas/isolation-manifest.js +16 -9
- package/dist/core/schemas/plan.d.ts +329 -3
- package/dist/core/schemas/plan.js +89 -3
- package/dist/core/schemas/pointer.d.ts +47 -1
- package/dist/core/schemas/pointer.js +89 -8
- package/dist/core/schemas/port-messages.d.ts +194 -10
- package/dist/core/schemas/port-messages.js +73 -6
- package/dist/core/schemas/probe-body.d.ts +18 -0
- package/dist/core/schemas/probe-body.js +13 -0
- package/dist/core/schemas/probe-qualification.d.ts +168 -0
- package/dist/core/schemas/probe-qualification.js +106 -0
- package/dist/core/schemas/probe.d.ts +431 -0
- package/dist/core/schemas/probe.js +6 -2
- package/dist/core/schemas/scoring-policy.d.ts +1 -0
- package/dist/core/schemas/scoring-policy.js +6 -1
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
- package/dist/core/schemas/sealed-run-record.d.ts +161 -12
- package/dist/core/schemas/sealed-run-record.js +92 -15
- package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
- package/dist/core/schemas/sensitivity-witness.js +61 -5
- package/dist/core/score/binding-order.d.ts +31 -0
- package/dist/core/score/binding-order.js +88 -0
- package/dist/core/score/bindings.d.ts +93 -0
- package/dist/core/score/bindings.js +309 -0
- package/dist/core/score/ladder-table.d.ts +23 -0
- package/dist/core/score/ladder-table.js +109 -0
- package/dist/core/score/ladder.d.ts +229 -0
- package/dist/core/score/ladder.js +464 -0
- package/dist/core/score/mode-agreement.d.ts +30 -0
- package/dist/core/score/mode-agreement.js +16 -0
- package/dist/core/score/outcome-table.d.ts +17 -0
- package/dist/core/score/outcome-table.js +172 -0
- package/dist/core/score/outcome.d.ts +466 -0
- package/dist/core/score/outcome.js +562 -0
- package/dist/core/score/qualification.d.ts +83 -0
- package/dist/core/score/qualification.js +645 -0
- package/dist/core/score/quotation.d.ts +56 -0
- package/dist/core/score/quotation.js +226 -0
- package/dist/core/score/reduce-trials.d.ts +80 -0
- package/dist/core/score/reduce-trials.js +90 -0
- package/dist/core/score/score.d.ts +72 -0
- package/dist/core/score/score.js +607 -0
- package/dist/core/score/selection.d.ts +80 -0
- package/dist/core/score/selection.js +98 -0
- package/dist/core/score/strength.d.ts +58 -0
- package/dist/core/score/strength.js +227 -0
- package/dist/core/score/witness.d.ts +138 -0
- package/dist/core/score/witness.js +320 -0
- package/dist/core/seal/derived-reference.d.ts +2 -9
- package/dist/core/seal/derived-reference.js +293 -46
- package/dist/core/seal/plan-index.d.ts +18 -3
- package/dist/core/seal/plan-index.js +44 -8
- package/dist/core/seal/seal.js +8 -3
- package/dist/core/stage-contracts.d.ts +62 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ports/environment-probe-port.d.ts +60 -4
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/probe-conformance.d.ts +29 -0
- package/dist/testing/probe-conformance.js +34 -5
- package/package.json +12 -3
- package/schemas/eval-contract.schema.json +1787 -797
- package/schemas/evidence-artifact.schema.json +126 -9
- package/schemas/isolation-manifest.schema.json +17 -10
- package/schemas/probe.schema.json +636 -5
- package/schemas/rubric.schema.json +1 -1
- package/schemas/scoring-policy.schema.json +8 -1
- package/schemas/sealed-evaluator-brief.schema.json +11 -1
- package/schemas/sealed-run-record.schema.json +314 -40
|
@@ -1,18 +1,115 @@
|
|
|
1
|
-
import { TRANSPORT_CHANNELS } from './schemas/pointer.js';
|
|
1
|
+
import { COMMAND_CHANNELS, INPUT_CHANNELS, TRANSPORT_CHANNELS, } from './schemas/pointer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which output channel the operation's one response descriptor describes.
|
|
4
|
+
*
|
|
5
|
+
* AD-19 gives every operation exactly one descriptor, and the channel that
|
|
6
|
+
* descriptor describes is what makes its keys addressable. For an interface
|
|
7
|
+
* that speaks HTTP that channel is the response body; a command operation
|
|
8
|
+
* declares its own. One rule, three roots: every consumer that used to hard-code
|
|
9
|
+
* `response-body` asks this instead.
|
|
10
|
+
*/
|
|
11
|
+
export const descriptorChannelOf = (operation) => {
|
|
12
|
+
if (!isCommandOperation(operation))
|
|
13
|
+
return 'response-body';
|
|
14
|
+
const { descriptorChannel } = operation;
|
|
15
|
+
return descriptorChannel.kind === 'stream'
|
|
16
|
+
? descriptorChannel.channel
|
|
17
|
+
: 'artifact';
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Which artifact the descriptor describes, or `null` when it describes a
|
|
21
|
+
* stream. An artifact pointer descends through the descriptor only when it
|
|
22
|
+
* names this one; every other declared artifact is known to exist and declares
|
|
23
|
+
* no structure.
|
|
24
|
+
*/
|
|
25
|
+
export const descriptorArtifactOf = (operation) => {
|
|
26
|
+
if (!isCommandOperation(operation))
|
|
27
|
+
return null;
|
|
28
|
+
const { descriptorChannel } = operation;
|
|
29
|
+
return descriptorChannel.kind === 'artifact'
|
|
30
|
+
? descriptorChannel.artifactId
|
|
31
|
+
: null;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Whether an evidence target addresses the channel this operation's response
|
|
35
|
+
* descriptor describes.
|
|
36
|
+
*
|
|
37
|
+
* The channel name alone is not the answer on the artifact channel: an
|
|
38
|
+
* operation may declare several files while its one descriptor describes one of
|
|
39
|
+
* them, so a pointer at a different file names a channel with no declared
|
|
40
|
+
* structure. Every consumer asks this rather than comparing the channel itself,
|
|
41
|
+
* because comparing only the channel was a defect at three separate sites and a
|
|
42
|
+
* fourth site would have made the same mistake for the same reason.
|
|
43
|
+
*/
|
|
44
|
+
export const targetsDescribedChannel = (operation, target) => {
|
|
45
|
+
if (target.channel !== descriptorChannelOf(operation))
|
|
46
|
+
return false;
|
|
47
|
+
if (target.channel !== 'artifact')
|
|
48
|
+
return true;
|
|
49
|
+
return target.artifactId === descriptorArtifactOf(operation);
|
|
50
|
+
};
|
|
51
|
+
/** Every artifact identifier the operation declares it writes. */
|
|
52
|
+
export const declaredArtifactsOf = (operation) => isCommandOperation(operation) ? operation.artifacts : [];
|
|
53
|
+
/**
|
|
54
|
+
* Which operation shape this is. Reads `invocation` rather than a kind field,
|
|
55
|
+
* because an operation does not carry its interface's kind; the two shapes are
|
|
56
|
+
* distinguished by a required field only one of them declares.
|
|
57
|
+
*/
|
|
58
|
+
export const isCommandOperation = (operation) => 'invocation' in operation;
|
|
59
|
+
/** The same question the other way round, for the callers that filter. */
|
|
60
|
+
export const isApiOperation = (operation) => !isCommandOperation(operation);
|
|
61
|
+
/**
|
|
62
|
+
* The operation's input channels paired with the shapes they declare.
|
|
63
|
+
*
|
|
64
|
+
* Callers take the pairs rather than a channel list they then index the
|
|
65
|
+
* request shape with. Indexing is what breaks under the operation union:
|
|
66
|
+
* TypeScript cannot prove that a channel name drawn from one kind's tuple is a
|
|
67
|
+
* key of the other kind's shape, and both ways around that are casts. Pairing
|
|
68
|
+
* dereferences the union once, here.
|
|
69
|
+
*/
|
|
70
|
+
export function requestChannelsOf(operation) {
|
|
71
|
+
if (isCommandOperation(operation)) {
|
|
72
|
+
return COMMAND_CHANNELS.map((channel) => ({
|
|
73
|
+
channel,
|
|
74
|
+
shape: operation.requestShape[channel],
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
return TRANSPORT_CHANNELS.map((channel) => ({
|
|
78
|
+
channel,
|
|
79
|
+
shape: operation.requestShape[channel],
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/** The shape one named channel declares, or `undefined` off this kind. */
|
|
83
|
+
export function requestShapeOf(operation, channel) {
|
|
84
|
+
return requestChannelsOf(operation).find((entry) => entry.channel === channel)
|
|
85
|
+
?.shape;
|
|
86
|
+
}
|
|
87
|
+
/** The channel names an operation of this kind may declare inputs on. */
|
|
88
|
+
export const inputChannelsOf = (operation) => isCommandOperation(operation) ? COMMAND_CHANNELS : TRANSPORT_CHANNELS;
|
|
89
|
+
/**
|
|
90
|
+
* A step's bound channels paired with what each binds, for the same reason
|
|
91
|
+
* `requestChannelsOf` pairs: `InputBinding` is a union and a channel name
|
|
92
|
+
* drawn from one branch is not a key of the other.
|
|
93
|
+
*
|
|
94
|
+
* Ordered by `INPUT_CHANNELS` rather than by the parsed object's own key
|
|
95
|
+
* order, so which binding a check reports never depends on how the document
|
|
96
|
+
* happened to be written.
|
|
97
|
+
*/
|
|
98
|
+
export function boundChannelsOf(binding) {
|
|
99
|
+
const present = binding;
|
|
100
|
+
return INPUT_CHANNELS.filter((channel) => channel in binding).map((channel) => ({ channel, bound: present[channel] ?? null }));
|
|
101
|
+
}
|
|
2
102
|
/**
|
|
3
103
|
* AD-10's exemption predicate. Any of a channel's three lists naming a key
|
|
4
104
|
* counts: a permitted-only or types-only channel is still a surface a witness
|
|
5
105
|
* can vary.
|
|
6
106
|
*/
|
|
7
107
|
export function declaresNoRequestKeys(operation) {
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
shape.permittedKeys.length === 0 &&
|
|
12
|
-
Object.keys(shape.types).length === 0);
|
|
13
|
-
});
|
|
108
|
+
return requestChannelsOf(operation).every(({ shape }) => shape.requiredKeys.length === 0 &&
|
|
109
|
+
shape.permittedKeys.length === 0 &&
|
|
110
|
+
Object.keys(shape.types).length === 0);
|
|
14
111
|
}
|
|
15
112
|
/** Whether no channel declares a required key. */
|
|
16
113
|
export function declaresNoRequiredKeys(operation) {
|
|
17
|
-
return
|
|
114
|
+
return requestChannelsOf(operation).every(({ shape }) => shape.requiredKeys.length === 0);
|
|
18
115
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScoredOutcomesAndVerdict } from '../score/score.ts';
|
|
2
|
+
import type { EmitStage } from '../stage-contracts.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The stage. Builds `scoringVersionInputs`/`scoringVersion`/
|
|
5
|
+
* `comparabilityKey`/the strength block/the remediation block once, shared
|
|
6
|
+
* between the two mode branches, then the mode-discriminated artifact
|
|
7
|
+
* literal, then calls `checkModeAgreement` immediately before returning.
|
|
8
|
+
*/
|
|
9
|
+
export declare const emit: EmitStage<ScoredOutcomesAndVerdict>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-24's emit stage: `score`'s widened product plus the three caller-
|
|
3
|
+
* attested digests AD-11 fixes, minting the `EvidenceArtifact` this package
|
|
4
|
+
* has never built until now. Pure per AD-1/AD-2, and total the way every
|
|
5
|
+
* other stage in this package is: nothing here throws for a domain input.
|
|
6
|
+
*
|
|
7
|
+
* `comparabilityKey`, `strength`, and `remediation` are lifted verbatim from
|
|
8
|
+
* `scripts/worked-example-target.ts`'s own hand-assembly (its former
|
|
9
|
+
* lines 1432-1487), the field-by-field precedent this module generalises
|
|
10
|
+
* from one caller-assembled record to `score`'s own widened product.
|
|
11
|
+
*/
|
|
12
|
+
import { digestArtifact } from '../canonical/digest.js';
|
|
13
|
+
import { freezeArtifact } from '../lineage/freeze.js';
|
|
14
|
+
import { EvidenceArtifact, } from '../schemas/evidence-artifact.js';
|
|
15
|
+
import { checkModeAgreement } from '../score/mode-agreement.js';
|
|
16
|
+
import { buildStrengthVector } from '../score/strength.js';
|
|
17
|
+
const SCORING_POLICY_ARTIFACT_PATH = 'ScoringPolicy';
|
|
18
|
+
const SCORING_VERSION_INPUTS_ARTIFACT_PATH = 'ScoringVersionInputs';
|
|
19
|
+
const COMPARABILITY_KEY_ARTIFACT_PATH = 'ComparabilityKey';
|
|
20
|
+
/**
|
|
21
|
+
* `mode` joins AD-11's three caller-attested digest names as a fourth
|
|
22
|
+
* member: like them, it has no artifact source anywhere in this pipeline and
|
|
23
|
+
* can only ever be caller-supplied, never re-derived by `score`.
|
|
24
|
+
*/
|
|
25
|
+
const CALLER_ATTESTED_INPUTS = ['corpusDigest', 'fixtureDigest', 'evaluatorConfigurationDigest', 'mode'];
|
|
26
|
+
/**
|
|
27
|
+
* The stage. Builds `scoringVersionInputs`/`scoringVersion`/
|
|
28
|
+
* `comparabilityKey`/the strength block/the remediation block once, shared
|
|
29
|
+
* between the two mode branches, then the mode-discriminated artifact
|
|
30
|
+
* literal, then calls `checkModeAgreement` immediately before returning.
|
|
31
|
+
*/
|
|
32
|
+
export const emit = (scored, corpusDigest, fixtureDigest, evaluatorConfigurationDigest) => {
|
|
33
|
+
const scoringPolicyDigest = digestArtifact(scored.policy, SCORING_POLICY_ARTIFACT_PATH);
|
|
34
|
+
// AD-7's declared key: the scoring policy digest plus the corpus digest
|
|
35
|
+
// restricted to the probes both results cover -- here, the admitted probe
|
|
36
|
+
// identifiers, sorted so array order never enters the digest.
|
|
37
|
+
const comparabilityKey = digestArtifact({
|
|
38
|
+
scoringPolicyDigest,
|
|
39
|
+
probeIds: scored.sealedProbes.admitted
|
|
40
|
+
.map((entry) => entry.probe.probeId)
|
|
41
|
+
.sort(),
|
|
42
|
+
}, COMPARABILITY_KEY_ARTIFACT_PATH);
|
|
43
|
+
const excludedProbeIds = scored.sealedProbes.rejected.map((entry) => entry.probe.probeId);
|
|
44
|
+
const { trials, outcomes: scoredOutcomes } = scored.assessment.outcomeState;
|
|
45
|
+
const trialCount = `${trials.completed} completed trial${trials.completed === 1 ? '' : 's'}`;
|
|
46
|
+
const unreachedOracles = scoredOutcomes.filter((outcome) => outcome.resolution.state === 'unreached');
|
|
47
|
+
// AD-21: a run that resolved `unreached` or completed fewer trials than
|
|
48
|
+
// the policy's declared minimum marks the vector non-comparable rather
|
|
49
|
+
// than silently comparing it.
|
|
50
|
+
const comparable = trials.completed >= trials.declaredMinimum && unreachedOracles.length === 0;
|
|
51
|
+
const strengthNote = [
|
|
52
|
+
`${scored.sealedProbes.admitted.length} admitted probe over ${trialCount}.`,
|
|
53
|
+
trials.completed < trials.declaredMinimum
|
|
54
|
+
? `Below the declared minimum of ${trials.declaredMinimum}.`
|
|
55
|
+
: null,
|
|
56
|
+
unreachedOracles.length > 0
|
|
57
|
+
? `${unreachedOracles.map((outcome) => outcome.oracleId).join(', ')} resolved unreached.`
|
|
58
|
+
: null,
|
|
59
|
+
comparable ? null : 'The vector is reported and marked non-comparable.',
|
|
60
|
+
]
|
|
61
|
+
.filter((part) => part !== null)
|
|
62
|
+
.join(' ');
|
|
63
|
+
const vector = buildStrengthVector(scored.sealedProbes.admitted, new Map([[scored.probe.probeId, scored.trialSetResult]]));
|
|
64
|
+
// `scored.ladder.verdict` is `Verdict | null`: `null` is AD-21's Invalid
|
|
65
|
+
// rung, which "never becomes a contract verdict" -- a run reaching that
|
|
66
|
+
// rung is the caller's own signal to stop before minting an artifact at
|
|
67
|
+
// all, exactly as `scripts/worked-example-target.ts`'s own
|
|
68
|
+
// `if (ladder.verdict === null) fail(...)` guard, immediately before its
|
|
69
|
+
// call into this stage, already enforces. `emit` throws nothing for a
|
|
70
|
+
// domain input (the one throw this stage carries is the mode-agreement
|
|
71
|
+
// check below), so a verdict-less ladder reaching this point is a
|
|
72
|
+
// precondition violation the same way `seal()`'s own null-direction
|
|
73
|
+
// guard is one call earlier in the pipeline: trusted rather than
|
|
74
|
+
// re-checked a second time.
|
|
75
|
+
const verdict = scored.ladder.verdict;
|
|
76
|
+
const commonFields = {
|
|
77
|
+
schemaVersion: 3,
|
|
78
|
+
parentDigest: null,
|
|
79
|
+
// v0 mints no revision path for an evidence artifact: every `emit`
|
|
80
|
+
// call is a lineage root, matching `seal.ts`'s own root-artifact
|
|
81
|
+
// precedent for the one other `lineage: 'mints'` stage with a built
|
|
82
|
+
// module.
|
|
83
|
+
revisionCount: 0,
|
|
84
|
+
runId: scored.runId,
|
|
85
|
+
comparabilityKey,
|
|
86
|
+
excludedProbeIds,
|
|
87
|
+
exitCode: scored.ladder.exitCode,
|
|
88
|
+
verdictBasis: [...scored.ladder.basis],
|
|
89
|
+
callerAttestedInputs: [...CALLER_ATTESTED_INPUTS],
|
|
90
|
+
trials,
|
|
91
|
+
outcomes: [...scored.outcomes],
|
|
92
|
+
uncitedFindings: [...scored.uncitedFindings],
|
|
93
|
+
coverageGaps: [...scored.assessment.coverageGaps],
|
|
94
|
+
strength: {
|
|
95
|
+
denominator: `unique qualified probe identifiers exercised per class, across ${trialCount}`,
|
|
96
|
+
// `matchProbeWitness` is the only reachable producer of a
|
|
97
|
+
// `Strength.basis` value and it always returns `'measured'`;
|
|
98
|
+
// `'reconstructed'`'s one producer is dead code in v0. No live
|
|
99
|
+
// second value to select between, so this is a constant.
|
|
100
|
+
basis: 'measured',
|
|
101
|
+
vector,
|
|
102
|
+
comparable,
|
|
103
|
+
note: strengthNote,
|
|
104
|
+
},
|
|
105
|
+
remediation: {
|
|
106
|
+
// AD-12's REMEDIATION count, distinct from this artifact's own root
|
|
107
|
+
// `revisionCount` above; the two are named identically only because
|
|
108
|
+
// they nest at different depths.
|
|
109
|
+
revisionCount: scored.contract.revisionCount,
|
|
110
|
+
cap: scored.policy.remediationCap,
|
|
111
|
+
// AD-12 has the package only validate the cap; nothing enforces it,
|
|
112
|
+
// so there is no second legal value.
|
|
113
|
+
capSource: 'caller-attested',
|
|
114
|
+
// `remediationState` is already declared `LineageChain` (`ladder.ts`):
|
|
115
|
+
// reused from the assessment, never recomputed here.
|
|
116
|
+
lineageChain: scored.assessment.remediationState,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Runs both of this stage's checks, then freezes and returns. AD-29: a
|
|
121
|
+
* stage freezes the artifact it owns, matching `seal.ts`/
|
|
122
|
+
* `preflight/reduce.ts`'s own precedent for the other two minting stages.
|
|
123
|
+
* Both checks fire only on a precondition violation, never a domain
|
|
124
|
+
* input: neither is reachable through any path this module's own
|
|
125
|
+
* construction takes.
|
|
126
|
+
*/
|
|
127
|
+
const finalize = (artifact) => {
|
|
128
|
+
const agreement = checkModeAgreement({ mode: scored.assessment.mode }, { mode: artifact.mode });
|
|
129
|
+
if (!agreement.agrees) {
|
|
130
|
+
// Reachable only through a type-system bypass: `artifact.mode` is
|
|
131
|
+
// stamped from `scored.assessment.mode` two lines above in every
|
|
132
|
+
// branch this function builds, so the two can disagree only if a
|
|
133
|
+
// caller assembled `scored` from two independently-sourced values
|
|
134
|
+
// -- the future caller this check exists for, mirroring
|
|
135
|
+
// `reduceTrialSet`'s own `TypeError`-for-bypass-only-input
|
|
136
|
+
// precedent.
|
|
137
|
+
throw new TypeError(`emit(): assembled an artifact whose mode ("${agreement.artifactMode}") disagrees with the assessment mode ("${agreement.recordMode}") it was built from`);
|
|
138
|
+
}
|
|
139
|
+
return freezeArtifact(validateAssembledArtifact(artifact));
|
|
140
|
+
};
|
|
141
|
+
if (scored.assessment.mode === 'production') {
|
|
142
|
+
const scoringVersionInputs = {
|
|
143
|
+
contractSchemaVersion: scored.contract.schemaVersion,
|
|
144
|
+
corpusDigest,
|
|
145
|
+
fixtureDigest,
|
|
146
|
+
evaluatorConfigurationDigest,
|
|
147
|
+
scoringPolicyDigest,
|
|
148
|
+
mode: 'production',
|
|
149
|
+
};
|
|
150
|
+
const artifact = {
|
|
151
|
+
...commonFields,
|
|
152
|
+
mode: 'production',
|
|
153
|
+
scoringVersionInputs,
|
|
154
|
+
scoringVersion: digestArtifact(scoringVersionInputs, SCORING_VERSION_INPUTS_ARTIFACT_PATH),
|
|
155
|
+
productionVerdict: verdict,
|
|
156
|
+
};
|
|
157
|
+
return finalize(artifact);
|
|
158
|
+
}
|
|
159
|
+
const scoringVersionInputs = {
|
|
160
|
+
contractSchemaVersion: scored.contract.schemaVersion,
|
|
161
|
+
corpusDigest,
|
|
162
|
+
fixtureDigest,
|
|
163
|
+
evaluatorConfigurationDigest,
|
|
164
|
+
scoringPolicyDigest,
|
|
165
|
+
mode: 'contract-scoring',
|
|
166
|
+
};
|
|
167
|
+
const artifact = {
|
|
168
|
+
...commonFields,
|
|
169
|
+
mode: 'contract-scoring',
|
|
170
|
+
scoringVersionInputs,
|
|
171
|
+
scoringVersion: digestArtifact(scoringVersionInputs, SCORING_VERSION_INPUTS_ARTIFACT_PATH),
|
|
172
|
+
contractVerdict: verdict,
|
|
173
|
+
uncitedFindingGaps: [...scored.assessment.uncitedDefectFindings],
|
|
174
|
+
systemRecommendationRecorded: scored.assessment.systemRecommendationRecorded,
|
|
175
|
+
systemRecommendationNote: scored.assessment.systemRecommendationNote,
|
|
176
|
+
};
|
|
177
|
+
return finalize(artifact);
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* The runtime backstop `finalize()` calls before freezing: TypeScript
|
|
181
|
+
* guarantees this module's own object literals match `EvidenceArtifact`'s
|
|
182
|
+
* shape at compile time, except at the one place that casts past it
|
|
183
|
+
* (`scored.ladder.verdict as Verdict`, above). Mirrors `seal.ts`'s own
|
|
184
|
+
* `validateAssembledBrief` -- same `safeParse`-then-`TypeError` shape, same
|
|
185
|
+
* first-issue diagnosis -- so a verdict that reached this function `null`
|
|
186
|
+
* despite the caller's own precondition fails loudly here instead of
|
|
187
|
+
* shipping a schema-invalid artifact silently.
|
|
188
|
+
*/
|
|
189
|
+
function validateAssembledArtifact(artifact) {
|
|
190
|
+
const result = EvidenceArtifact.safeParse(artifact);
|
|
191
|
+
if (result.success)
|
|
192
|
+
return result.data;
|
|
193
|
+
const issueCount = result.error.issues.length;
|
|
194
|
+
const firstIssue = result.error.issues[0];
|
|
195
|
+
const firstPath = firstIssue ? dotPath(firstIssue.path) : '';
|
|
196
|
+
throw new TypeError(`emit(): assembled an artifact that failed EvidenceArtifact validation: ${issueCount} issue${issueCount === 1 ? '' : 's'}, first at "${firstPath === '' ? '(root)' : firstPath}"`, { cause: result.error });
|
|
197
|
+
}
|
|
198
|
+
/** `seal.ts`'s own formatter, duplicated rather than shared: both are small, stage-local, and neither exports it today. */
|
|
199
|
+
function dotPath(path) {
|
|
200
|
+
const segments = [];
|
|
201
|
+
for (const segment of path) {
|
|
202
|
+
if (typeof segment === 'number')
|
|
203
|
+
segments.push(`[${segment}]`);
|
|
204
|
+
else if (typeof segment === 'symbol' || /[^\w$]/.test(segment)) {
|
|
205
|
+
segments.push(`[${JSON.stringify(String(segment))}]`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
if (segments.length > 0)
|
|
209
|
+
segments.push('.');
|
|
210
|
+
segments.push(segment);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return segments.join('');
|
|
214
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PrivateArtifactManifest } from '../schemas/private-artifact-manifest.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Throws `RuntimeFault('digest-mismatch', ...)` on the first
|
|
4
|
+
* `entries[i]` whose declared `digest` disagrees with
|
|
5
|
+
* `resolvedDigests.get(entry.privateRef)`. An entry with no resolved digest
|
|
6
|
+
* at all -- `resolvedDigests` carries no key for its `privateRef` -- is the
|
|
7
|
+
* same disagreement: there is no byte the manifest's declared digest can be
|
|
8
|
+
* checked against, which this comparator treats no differently from a
|
|
9
|
+
* genuine mismatch.
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkPrivateArtifactManifestDigests(manifest: PrivateArtifactManifest, resolvedDigests: ReadonlyMap<string, string>): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of `private-artifact-manifest.ts`'s own "a mismatch is an
|
|
3
|
+
* AD-28 `digest-mismatch` fault": given a manifest and each entry's already-
|
|
4
|
+
* resolved digest, throws on the first disagreement. AD-8 requires the
|
|
5
|
+
* digest recomputed from the resolved bytes; resolving those bytes needs
|
|
6
|
+
* `CorpusPort.resolve`, an async port method, and AD-34 makes awaiting a
|
|
7
|
+
* port `application/`'s job, never `core/`'s. This module is the comparator
|
|
8
|
+
* alone and has no caller yet: whoever first awaits `CorpusPort.resolve` to
|
|
9
|
+
* build `resolvedDigests` is this function's caller, and that await belongs
|
|
10
|
+
* in `application/`, not here.
|
|
11
|
+
*/
|
|
12
|
+
import { RuntimeFault } from '../schemas/faults.js';
|
|
13
|
+
/**
|
|
14
|
+
* Throws `RuntimeFault('digest-mismatch', ...)` on the first
|
|
15
|
+
* `entries[i]` whose declared `digest` disagrees with
|
|
16
|
+
* `resolvedDigests.get(entry.privateRef)`. An entry with no resolved digest
|
|
17
|
+
* at all -- `resolvedDigests` carries no key for its `privateRef` -- is the
|
|
18
|
+
* same disagreement: there is no byte the manifest's declared digest can be
|
|
19
|
+
* checked against, which this comparator treats no differently from a
|
|
20
|
+
* genuine mismatch.
|
|
21
|
+
*/
|
|
22
|
+
export function checkPrivateArtifactManifestDigests(manifest, resolvedDigests) {
|
|
23
|
+
manifest.entries.forEach((entry, index) => {
|
|
24
|
+
const resolved = resolvedDigests.get(entry.privateRef);
|
|
25
|
+
if (resolved === entry.digest)
|
|
26
|
+
return;
|
|
27
|
+
throw new RuntimeFault('digest-mismatch', `PrivateArtifactManifest.entries[${index}]`, `entry privateRef "${entry.privateRef}" declares digest "${entry.digest}", but the resolved bytes digest to ${resolved === undefined
|
|
28
|
+
? 'nothing (no resolved digest supplied for this privateRef)'
|
|
29
|
+
: `"${resolved}"`}`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
9
9
|
import type { JsonValue } from '../schemas/primitives.ts';
|
|
10
10
|
import type { Observation } from '../schemas/sealed-run-record.ts';
|
|
11
|
-
import { type PlanIndex } from '../seal/plan-index.ts';
|
|
12
|
-
import type { PointerDenotesCollection, ResolveOperand } from './resolution.ts';
|
|
11
|
+
import { type EvidenceTarget, type PlanIndex } from '../seal/plan-index.ts';
|
|
12
|
+
import type { PointerDenotesCollection, ReferenceSetKeys, ResolveOperand } from './resolution.ts';
|
|
13
13
|
import { type ResolvedValue } from './resolved-value.ts';
|
|
14
14
|
/**
|
|
15
15
|
* A canonical RFC 6901 array-index token: no leading zero except "0" itself,
|
|
@@ -26,7 +26,7 @@ export declare const ARRAY_INDEX_PATTERN: RegExp;
|
|
|
26
26
|
* falls through to `Object.prototype`. Any miss, type mismatch, or tail
|
|
27
27
|
* running past a scalar collapses to `ABSENT` uniformly (AD-26).
|
|
28
28
|
*/
|
|
29
|
-
export declare function walkTail(root:
|
|
29
|
+
export declare function walkTail(root: ResolvedValue, tail: readonly string[]): ResolvedValue;
|
|
30
30
|
/**
|
|
31
31
|
* Decodes `BoundElementPointer`'s tail. "@/" is the shortest legal pointer
|
|
32
32
|
* form and denotes the element itself. `decodeTail` special-cases only a
|
|
@@ -34,6 +34,7 @@ export declare function walkTail(root: JsonValue, tail: readonly string[]): Reso
|
|
|
34
34
|
* calling it.
|
|
35
35
|
*/
|
|
36
36
|
export declare function decodeBoundElementTail(pointer: string): readonly string[];
|
|
37
|
+
export declare function channelRoot(observation: Observation, target: EvidenceTarget): ResolvedValue;
|
|
37
38
|
/**
|
|
38
39
|
* The `ResolveOperand`. `stepObservations` holds one already-selected
|
|
39
40
|
* `Observation` per interaction step; `referenceSets` mirrors the contract's
|
|
@@ -42,6 +43,12 @@ export declare function decodeBoundElementTail(pointer: string): readonly string
|
|
|
42
43
|
* prototype-chain gotcha `walkTail` guards against.
|
|
43
44
|
*/
|
|
44
45
|
export declare function makeResolveOperand(stepObservations: Readonly<Record<string, Observation>>, referenceSets: Readonly<Record<string, JsonValue[]>>): ResolveOperand;
|
|
46
|
+
/**
|
|
47
|
+
* The `keys` a contract's reference sets declare. Built from the same
|
|
48
|
+
* `contract.referenceSets` the members map handed to `makeResolveOperand` is
|
|
49
|
+
* built from, so a set that resolves to members always has its keys here.
|
|
50
|
+
*/
|
|
51
|
+
export declare function referenceSetKeysOf(contract: EvalContract): ReferenceSetKeys;
|
|
45
52
|
/**
|
|
46
53
|
* Only `response-body` can ever answer `true` (AD-19: `collectionLocations`
|
|
47
54
|
* is the only declared-collection surface, scoped to the body alone). The
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* bound-element `@/` form, and walk it into an already-selected
|
|
6
6
|
* `Observation`.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { targetsDescribedChannel } from '../declared-inputs.js';
|
|
9
|
+
import { anyOperationOf, buildPlanIndex, decodeTail, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
9
10
|
import { ABSENT } from './resolved-value.js';
|
|
10
11
|
/**
|
|
11
12
|
* A canonical RFC 6901 array-index token: no leading zero except "0" itself,
|
|
@@ -23,6 +24,12 @@ export const ARRAY_INDEX_PATTERN = /^(?:0|[1-9][0-9]*)$/;
|
|
|
23
24
|
* running past a scalar collapses to `ABSENT` uniformly (AD-26).
|
|
24
25
|
*/
|
|
25
26
|
export function walkTail(root, tail) {
|
|
27
|
+
// A root that did not resolve stays unresolved however short the tail is.
|
|
28
|
+
// The bare-pointer case is the one that matters: without this, an absent
|
|
29
|
+
// channel with no tail returned its own root and a caller could not tell it
|
|
30
|
+
// from a resolved value.
|
|
31
|
+
if (root === ABSENT)
|
|
32
|
+
return ABSENT;
|
|
26
33
|
let current = root;
|
|
27
34
|
for (const token of tail) {
|
|
28
35
|
if (current === null || typeof current !== 'object')
|
|
@@ -53,12 +60,30 @@ export function decodeBoundElementTail(pointer) {
|
|
|
53
60
|
return tailSource === '/' ? [] : decodeTail(tailSource);
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
|
-
* Selects the channel `target` names off one `Observation`.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
63
|
+
* Selects the channel `target` names off one `Observation`.
|
|
64
|
+
*
|
|
65
|
+
* `stdout`, `stderr`, and each written artifact are tagged, so a channel a
|
|
66
|
+
* harness captured as JSON resolves to the value and a tail walks into it,
|
|
67
|
+
* while one captured as text resolves to the string and a tail over it
|
|
68
|
+
* resolves `ABSENT`. That is what makes a command operation's nominated output
|
|
69
|
+
* channel resolvable at score time on the same terms as a response body.
|
|
70
|
+
*
|
|
71
|
+
* Exported so `core/score/bindings.ts` reads a captured pointer's channel off
|
|
72
|
+
* an observation through this one spelling, keeping the switch in one place.
|
|
60
73
|
*/
|
|
61
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The value a tagged observed channel resolves to.
|
|
76
|
+
*
|
|
77
|
+
* A JSON body resolves to the value itself, so a tail walks into it; text
|
|
78
|
+
* resolves to the string, so a tail over it resolves `ABSENT` through
|
|
79
|
+
* `walkTail`, which is the truthful answer for output nothing parsed. An absent
|
|
80
|
+
* channel resolves to `ABSENT` and never to `null`: the tag exists precisely to
|
|
81
|
+
* tell "nothing was observed here" from "a value that was JSON null", and
|
|
82
|
+
* collapsing the two would make `existence` answer true for a file the run
|
|
83
|
+
* never wrote, since AD-26 counts `null` as present.
|
|
84
|
+
*/
|
|
85
|
+
const observedValue = (body) => body.kind === 'absent' ? ABSENT : body.value;
|
|
86
|
+
export function channelRoot(observation, target) {
|
|
62
87
|
switch (target.channel) {
|
|
63
88
|
case 'response-body':
|
|
64
89
|
return observation.responseBody;
|
|
@@ -67,9 +92,9 @@ function channelRoot(observation, target) {
|
|
|
67
92
|
case 'response-status':
|
|
68
93
|
return observation.responseStatus;
|
|
69
94
|
case 'stdout':
|
|
70
|
-
return observation.stdout;
|
|
95
|
+
return observedValue(observation.stdout);
|
|
71
96
|
case 'stderr':
|
|
72
|
-
return observation.stderr;
|
|
97
|
+
return observedValue(observation.stderr);
|
|
73
98
|
case 'exit-code':
|
|
74
99
|
return observation.exitCode;
|
|
75
100
|
case 'call-inputs': {
|
|
@@ -81,6 +106,27 @@ function channelRoot(observation, target) {
|
|
|
81
106
|
}
|
|
82
107
|
return observation.callInputs[transportChannel];
|
|
83
108
|
}
|
|
109
|
+
case 'artifact': {
|
|
110
|
+
const { artifactId } = target;
|
|
111
|
+
if (artifactId === null) {
|
|
112
|
+
// parseEvidenceTarget sets artifactId exactly when the channel
|
|
113
|
+
// is 'artifact', so this throw should never fire.
|
|
114
|
+
throw new TypeError('artifact evidence target names no artifact');
|
|
115
|
+
}
|
|
116
|
+
// `Object.hasOwn` for the reason this module's header gives for
|
|
117
|
+
// `stepObservations` and `referenceSets`: `Identifier` admits
|
|
118
|
+
// `constructor`, `toString` and `valueOf`, so a bare index on an
|
|
119
|
+
// unwritten file of one of those names resolves to an inherited
|
|
120
|
+
// function rather than to nothing.
|
|
121
|
+
//
|
|
122
|
+
// A file the run did not write is absent evidence, not a `null`
|
|
123
|
+
// value. `null` would read as present under AD-26 and invert every
|
|
124
|
+
// oracle asserting the file exists or does not.
|
|
125
|
+
if (!Object.hasOwn(observation.artifacts, artifactId))
|
|
126
|
+
return ABSENT;
|
|
127
|
+
const written = observation.artifacts[artifactId];
|
|
128
|
+
return written === undefined ? ABSENT : observedValue(written);
|
|
129
|
+
}
|
|
84
130
|
}
|
|
85
131
|
}
|
|
86
132
|
/**
|
|
@@ -115,6 +161,17 @@ export function makeResolveOperand(stepObservations, referenceSets) {
|
|
|
115
161
|
return walkTail(channelRoot(observation, target), target.tail);
|
|
116
162
|
};
|
|
117
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* The `keys` a contract's reference sets declare. Built from the same
|
|
166
|
+
* `contract.referenceSets` the members map handed to `makeResolveOperand` is
|
|
167
|
+
* built from, so a set that resolves to members always has its keys here.
|
|
168
|
+
*/
|
|
169
|
+
export function referenceSetKeysOf(contract) {
|
|
170
|
+
return Object.fromEntries(Object.entries(contract.referenceSets ?? {}).map(([id, set]) => [
|
|
171
|
+
id,
|
|
172
|
+
set.keys,
|
|
173
|
+
]));
|
|
174
|
+
}
|
|
118
175
|
function tokensEqual(a, b) {
|
|
119
176
|
return a.length === b.length && a.every((token, index) => token === b[index]);
|
|
120
177
|
}
|
|
@@ -137,14 +194,20 @@ export function makePointerDenotesCollection(contract, providedIndex) {
|
|
|
137
194
|
if (pointer.startsWith('@'))
|
|
138
195
|
return false;
|
|
139
196
|
const target = parseEvidenceTarget(pointer);
|
|
140
|
-
if (target.channel !== 'response-body')
|
|
141
|
-
return false;
|
|
142
197
|
const step = getIndex().stepOf(target.stepId);
|
|
143
198
|
if (step === undefined)
|
|
144
199
|
return false;
|
|
145
|
-
const operation = getIndex()
|
|
200
|
+
const operation = anyOperationOf(getIndex(), step.operationId);
|
|
146
201
|
if (operation === undefined)
|
|
147
202
|
return false;
|
|
203
|
+
// The channel is tested against the operation's own descriptor rather
|
|
204
|
+
// than against `response-body`. AD-4's empty-collection resolution
|
|
205
|
+
// applies to whichever channel an operation says carries its declared
|
|
206
|
+
// collections, and hard-coding the body left it inapplicable to every
|
|
207
|
+
// command contract: a quantifier over an empty declared collection
|
|
208
|
+
// resolved `false` instead of `insufficient-evidence`.
|
|
209
|
+
if (!targetsDescribedChannel(operation, target))
|
|
210
|
+
return false;
|
|
148
211
|
const { collectionLocations } = operation.responseDescriptor;
|
|
149
212
|
if (collectionLocations === null)
|
|
150
213
|
return false;
|
|
@@ -31,6 +31,18 @@ export declare function setMembership(value: ResolvedValue, set: JsonValue[], ar
|
|
|
31
31
|
* single element to search for: this function receives resolved values only,
|
|
32
32
|
* so it cannot tell a `{ literal }` array from a resolved `{ referenceSet }`.
|
|
33
33
|
* Known, accepted limitation.
|
|
34
|
+
*
|
|
35
|
+
* A separate property, and a separate limitation: a `{ referenceSet }`
|
|
36
|
+
* candidate matches **whole declared members** by structural equality. A
|
|
37
|
+
* container element carrying any field the reference set does not declare
|
|
38
|
+
* therefore never matches, which is the ordinary case for rows read off a
|
|
39
|
+
* response body. `set-membership`'s set position projects a reference set to
|
|
40
|
+
* its single declared key and this position does not, because projecting only
|
|
41
|
+
* where a container happens to hold objects would make the operand's meaning
|
|
42
|
+
* depend on the evidence's runtime shape. An author reconciling rows against a
|
|
43
|
+
* declared set wants `covers-by-key`, which compares on named keys on both
|
|
44
|
+
* sides, or `for-all(rows, set-membership(@/key, { referenceSet }))`, which
|
|
45
|
+
* reads the declared key.
|
|
34
46
|
*/
|
|
35
47
|
export declare function containment(container: ResolvedValue, candidate: ResolvedValue | JsonValue[], artifactPath: string): boolean;
|
|
36
48
|
/**
|
|
@@ -62,6 +74,16 @@ export declare function countTolerance(collection: ResolvedValue, expected: numb
|
|
|
62
74
|
* that is required and not permitted reaches here and fails every value.
|
|
63
75
|
*/
|
|
64
76
|
export declare function shape(value: ResolvedValue, descriptor: KeyedShapeDescriptor, _artifactPath: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Own-property lookup only, so a key like `__proto__` reads as missing
|
|
79
|
+
* rather than inherited. Returns `ABSENT`, never throws (AD-4: a missing key
|
|
80
|
+
* resolves `false`, not an error).
|
|
81
|
+
*
|
|
82
|
+
* Exported so `resolution.ts`'s `set-membership` set-operand projection reads
|
|
83
|
+
* a declared member's key through this one spelling, keeping the own-property
|
|
84
|
+
* guard in one place.
|
|
85
|
+
*/
|
|
86
|
+
export declare function keyValueOf(element: JsonValue, key: string): JsonValue | typeof ABSENT;
|
|
65
87
|
/**
|
|
66
88
|
* AD-4's bijection: equal cardinality and a distinct `actual` match per
|
|
67
89
|
* `expected` element on the named keys. `ABSENT` on either side resolves
|
|
@@ -90,6 +90,18 @@ export function setMembership(value, set, artifactPath) {
|
|
|
90
90
|
* single element to search for: this function receives resolved values only,
|
|
91
91
|
* so it cannot tell a `{ literal }` array from a resolved `{ referenceSet }`.
|
|
92
92
|
* Known, accepted limitation.
|
|
93
|
+
*
|
|
94
|
+
* A separate property, and a separate limitation: a `{ referenceSet }`
|
|
95
|
+
* candidate matches **whole declared members** by structural equality. A
|
|
96
|
+
* container element carrying any field the reference set does not declare
|
|
97
|
+
* therefore never matches, which is the ordinary case for rows read off a
|
|
98
|
+
* response body. `set-membership`'s set position projects a reference set to
|
|
99
|
+
* its single declared key and this position does not, because projecting only
|
|
100
|
+
* where a container happens to hold objects would make the operand's meaning
|
|
101
|
+
* depend on the evidence's runtime shape. An author reconciling rows against a
|
|
102
|
+
* declared set wants `covers-by-key`, which compares on named keys on both
|
|
103
|
+
* sides, or `for-all(rows, set-membership(@/key, { referenceSet }))`, which
|
|
104
|
+
* reads the declared key.
|
|
93
105
|
*/
|
|
94
106
|
export function containment(container, candidate, artifactPath) {
|
|
95
107
|
if (container === ABSENT)
|
|
@@ -287,8 +299,12 @@ export function shape(value, descriptor, _artifactPath) {
|
|
|
287
299
|
* Own-property lookup only, so a key like `__proto__` reads as missing
|
|
288
300
|
* rather than inherited. Returns `ABSENT`, never throws (AD-4: a missing key
|
|
289
301
|
* resolves `false`, not an error).
|
|
302
|
+
*
|
|
303
|
+
* Exported so `resolution.ts`'s `set-membership` set-operand projection reads
|
|
304
|
+
* a declared member's key through this one spelling, keeping the own-property
|
|
305
|
+
* guard in one place.
|
|
290
306
|
*/
|
|
291
|
-
function keyValueOf(element, key) {
|
|
307
|
+
export function keyValueOf(element, key) {
|
|
292
308
|
if (!isPlainObject(element) || !Object.hasOwn(element, key))
|
|
293
309
|
return ABSENT;
|
|
294
310
|
// `Object.hasOwn` above already proves the key is present; `noUncheckedIndexedAccess`
|