@volter/project 0.0.0-stage → 0.5.204
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 +202 -0
- package/NOTICE +6 -0
- package/README.md +18 -3
- package/package.json +64 -4
- package/schemas/engine-api.json +57 -0
- package/schemas/engine-api.md +22 -0
- package/schemas/volter-project.schema.json +468 -0
- package/schemas/volter-settings.schema.json +149 -0
- package/src/adapter/adapter-module.ts +997 -0
- package/src/adapter/adapter-surface.ts +34 -0
- package/src/adapter/authoring-seam-contract.ts +224 -0
- package/src/adapter/authoring.ts +1336 -0
- package/src/adapter/binding.ts +445 -0
- package/src/adapter/debug-registry-ref.ts +19 -0
- package/src/adapter/editor-looks.ts +519 -0
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/registry.ts +116 -0
- package/src/adapter/frame-capture.ts +80 -0
- package/src/adapter/host-context.ts +114 -0
- package/src/adapter/index.ts +209 -0
- package/src/adapter/ingest/contract-system-slots.ts +28 -0
- package/src/adapter/ingest/game-contract-seams.ts +86 -0
- package/src/adapter/ingest/game-contract.ts +506 -0
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/loop-gate-report.ts +53 -0
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-entry-surface.ts +100 -0
- package/src/adapter/physics-adapter-2d.ts +30 -0
- package/src/adapter/render-audio.ts +20 -0
- package/src/adapter/render-memory.ts +34 -0
- package/src/adapter/renderer-config.ts +84 -0
- package/src/adapter/root-adapter.ts +243 -0
- package/src/adapter/root-seam-contract.ts +61 -0
- package/src/adapter/root-stacking.ts +67 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/system-adapter.ts +865 -0
- package/src/adapter/system-seam-contract.ts +138 -0
- package/src/adapter/system-slot.ts +301 -0
- package/src/adapter/transform.ts +35 -0
- package/src/adapter/workspace-arrangement.ts +92 -0
- package/src/core/system-phase.ts +52 -0
- package/src/inspection.ts +281 -0
- package/src/manifest/configuration-kinds.ts +241 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +45 -0
- package/src/manifest/kind-modules.ts +58 -0
- package/src/manifest/load-file.ts +33 -0
- package/src/manifest/load.ts +380 -0
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/runtime-environment.ts +48 -0
- package/src/manifest/schema.ts +690 -0
- package/src/settings/keys.ts +152 -0
- package/src/settings/schema.ts +151 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Exhaustive proof requirements for substrate mount/lifecycle seams. */
|
|
2
|
+
|
|
3
|
+
import type { AdapterSurface } from './adapter-surface';
|
|
4
|
+
import type {
|
|
5
|
+
MountedCanvasRoot,
|
|
6
|
+
MountedReactRoot,
|
|
7
|
+
MountedRootBase,
|
|
8
|
+
MountedThreeRoot,
|
|
9
|
+
RootAdapter,
|
|
10
|
+
RootStateObserver,
|
|
11
|
+
} from './root-adapter';
|
|
12
|
+
import { defineSeamShape } from './seam-evidence';
|
|
13
|
+
|
|
14
|
+
export const ROOT_ADAPTER_SHAPE = defineSeamShape<RootAdapter<AdapterSurface>>()({
|
|
15
|
+
id: { optional: false, kind: 'value', required: 'shape' },
|
|
16
|
+
mount: { optional: false, kind: 'function', required: 'effect' },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const MOUNTED_ROOT_BASE_SHAPE = defineSeamShape<MountedRootBase>()({
|
|
20
|
+
drivesOwnLoop: { optional: false, kind: 'value', required: 'shape' },
|
|
21
|
+
updateCadence: { optional: true, kind: 'value', required: 'shape' },
|
|
22
|
+
update: { optional: true, kind: 'function', required: 'effect' },
|
|
23
|
+
fixedUpdate: { optional: true, kind: 'function', required: 'effect' },
|
|
24
|
+
setPaused: { optional: true, kind: 'function', required: 'effect' },
|
|
25
|
+
step: { optional: true, kind: 'function', required: 'effect' },
|
|
26
|
+
resize: { optional: true, kind: 'function', required: 'effect' },
|
|
27
|
+
hitTest: { optional: true, kind: 'function', required: 'operation' },
|
|
28
|
+
dispose: { optional: false, kind: 'function', required: 'effect' },
|
|
29
|
+
disposeComplete: { optional: true, kind: 'value', required: 'effect' },
|
|
30
|
+
authoring: { optional: true, kind: 'value', required: 'operation' },
|
|
31
|
+
systems: { optional: true, kind: 'value', required: 'operation' },
|
|
32
|
+
systemScope: { optional: true, kind: 'value', required: 'operation' },
|
|
33
|
+
observe: { optional: true, kind: 'value', required: 'effect' },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const ROOT_STATE_OBSERVER_SHAPE = defineSeamShape<RootStateObserver>()({
|
|
37
|
+
subscribe: { optional: false, kind: 'function', required: 'effect' },
|
|
38
|
+
snapshot: { optional: false, kind: 'function', required: 'operation' },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
type ThreeSurface = Omit<MountedThreeRoot<object, object>, keyof MountedRootBase>;
|
|
42
|
+
type CanvasSurface = Omit<MountedCanvasRoot, keyof MountedRootBase>;
|
|
43
|
+
type DomSurface = Omit<MountedReactRoot, keyof MountedRootBase>;
|
|
44
|
+
|
|
45
|
+
export const MOUNTED_THREE_SURFACE_SHAPE = defineSeamShape<ThreeSurface>()({
|
|
46
|
+
kind: { optional: false, kind: 'value', required: 'shape' },
|
|
47
|
+
scene: { optional: false, kind: 'value', required: 'effect' },
|
|
48
|
+
camera: { optional: false, kind: 'value', required: 'effect' },
|
|
49
|
+
rendererConfig: { optional: true, kind: 'value', required: 'shape' },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const MOUNTED_CANVAS_SURFACE_SHAPE = defineSeamShape<CanvasSurface>()({
|
|
53
|
+
kind: { optional: false, kind: 'value', required: 'shape' },
|
|
54
|
+
canvas: { optional: false, kind: 'value', required: 'effect' },
|
|
55
|
+
substrate: { optional: false, kind: 'value', required: 'effect' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const MOUNTED_DOM_SURFACE_SHAPE = defineSeamShape<DomSurface>()({
|
|
59
|
+
kind: { optional: false, kind: 'value', required: 'shape' },
|
|
60
|
+
container: { optional: false, kind: 'value', required: 'effect' },
|
|
61
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH ROOT OWNS A POINT — the pure half of D5 §2a, a rule about how several
|
|
3
|
+
* adapter roots share one surface, so it belongs beside the other seam
|
|
4
|
+
* contracts rather than inside the runtime that first needed it.
|
|
5
|
+
*
|
|
6
|
+
* Two callers with nothing else in common depend on it: the engine's
|
|
7
|
+
* `runtime/input-router.ts`, which wires it to real canvases and forwards
|
|
8
|
+
* claimed events; and the EDITOR, which asks the same question at design time
|
|
9
|
+
* with no game running (`authoring/layered-pick.ts` picks through stacked
|
|
10
|
+
* authoring layers, `authoring/design-time-layers.ts` orders the layers it
|
|
11
|
+
* mounts). That second caller is why the file moved here 2026-09-18
|
|
12
|
+
* (WORK.md §The open-source launch, phase 1 unit 7): the host may import the
|
|
13
|
+
* adapter CONTRACT and must not import the game runtime, and this was the one
|
|
14
|
+
* `runtime/` module left in its closure. No DOM, no canvas, no mount.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The pure decision inputs — no DOM. */
|
|
18
|
+
export interface ClaimEntry {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly zOrder: number;
|
|
21
|
+
/**
|
|
22
|
+
* Optional claim predicate over a point RELATIVE TO THE CONTAINER. Absent
|
|
23
|
+
* means: "this world claims only if it is the bottom (lowest zOrder)
|
|
24
|
+
* world" — D5 §2a's stated default ("transparent upper roots do not
|
|
25
|
+
* claim, the bottom world claims everything").
|
|
26
|
+
*/
|
|
27
|
+
readonly hitTest?: ((x: number, y: number) => boolean) | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bottom-to-top paint order: ascending `zOrder`, ties -> original array
|
|
32
|
+
* order — the SAME rule `manifest/load.ts`'s `loadGameManifest` sorts
|
|
33
|
+
* roots by, so a world's DOM stacking position always matches its
|
|
34
|
+
* manifest-resolved zOrder. The last element is the topmost (visually on
|
|
35
|
+
* top / highest z-index).
|
|
36
|
+
*/
|
|
37
|
+
export function stackOrder<T extends ClaimEntry>(entries: readonly T[]): T[] {
|
|
38
|
+
return entries
|
|
39
|
+
.map((entry, index) => ({ entry, index }))
|
|
40
|
+
.sort((a, b) => a.entry.zOrder - b.entry.zOrder || a.index - b.index)
|
|
41
|
+
.map(({ entry }) => entry);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve which world's id claims a container-relative point `(x, y)` — D5
|
|
46
|
+
* §2a: walk top-down (topmost first); the first world whose `hitTest`
|
|
47
|
+
* returns true wins ("topmost-claim wins"). A world with no `hitTest` never
|
|
48
|
+
* claims UNLESS it is the bottom-most world, which claims unconditionally
|
|
49
|
+
* (or via its own `hitTest`, if it declares one) — "no-claim falls to
|
|
50
|
+
* bottom". Returns `null` only when `entries` is empty.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveClaimingRoot(
|
|
53
|
+
entries: readonly ClaimEntry[],
|
|
54
|
+
x: number,
|
|
55
|
+
y: number,
|
|
56
|
+
): string | null {
|
|
57
|
+
if (entries.length === 0) return null;
|
|
58
|
+
const order = stackOrder(entries); // bottom -> top
|
|
59
|
+
for (let i = order.length - 1; i >= 1; i--) {
|
|
60
|
+
const entry = order[i]!;
|
|
61
|
+
if (entry.hitTest?.(x, y)) return entry.id;
|
|
62
|
+
}
|
|
63
|
+
// No upper world claimed (or there is only one world) -> the bottom world
|
|
64
|
+
// claims — honoring its own hitTest if it declares one (rare: the bottom
|
|
65
|
+
// world usually claims everything unconditionally per D5 §2a's default).
|
|
66
|
+
return order[0]!.id;
|
|
67
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence that a protocol seam tells the truth.
|
|
3
|
+
*
|
|
4
|
+
* A property existing on an object proves only SHAPE. It does not prove that
|
|
5
|
+
* calling it works, that its effect reaches the consumer, or that an authored
|
|
6
|
+
* effect survives a cold remount. Those are deliberately separate stages so a
|
|
7
|
+
* declaration can never be promoted to a green capability merely because it
|
|
8
|
+
* has the right spelling.
|
|
9
|
+
*
|
|
10
|
+
* Receipts are scoped to a SUBJECT and an EPOCH. A provider from a prior mount
|
|
11
|
+
* is not evidence about the current mount, even when both mounts use the same
|
|
12
|
+
* project and node ids. This is the generic answer to stale adapter registries,
|
|
13
|
+
* late provider registration, and project switching: old receipts simply do
|
|
14
|
+
* not participate in the new verdict.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const SEAM_PROOF_STAGES = ['claim', 'shape', 'operation', 'effect', 'round-trip'] as const;
|
|
18
|
+
|
|
19
|
+
export type SeamProofStage = (typeof SEAM_PROOF_STAGES)[number];
|
|
20
|
+
export type SeamEvidenceOutcome = 'pass' | 'fail' | 'absent';
|
|
21
|
+
export type SeamEvidenceSource =
|
|
22
|
+
| 'declaration'
|
|
23
|
+
| 'shape-check'
|
|
24
|
+
| 'consumer'
|
|
25
|
+
| 'conformance-probe'
|
|
26
|
+
| 'round-trip';
|
|
27
|
+
|
|
28
|
+
/** One directly observed fact. `detail` names the observation, never a claim
|
|
29
|
+
* such as "supported" with no described bar behind it. */
|
|
30
|
+
export interface SeamEvidenceReceipt {
|
|
31
|
+
readonly seam: string;
|
|
32
|
+
readonly subject: string;
|
|
33
|
+
readonly epoch: string;
|
|
34
|
+
readonly stage: SeamProofStage;
|
|
35
|
+
readonly outcome: SeamEvidenceOutcome;
|
|
36
|
+
readonly source: SeamEvidenceSource;
|
|
37
|
+
readonly detail: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type SeamEvidenceState = 'verified' | 'failed' | 'absent' | 'unverified' | 'not-applicable';
|
|
41
|
+
|
|
42
|
+
export interface SeamEvidenceVerdict {
|
|
43
|
+
readonly seam: string;
|
|
44
|
+
readonly subject: string;
|
|
45
|
+
readonly epoch: string;
|
|
46
|
+
readonly required: SeamProofStage;
|
|
47
|
+
readonly state: SeamEvidenceState;
|
|
48
|
+
/** The strongest current-epoch receipt, or null when nobody has measured the
|
|
49
|
+
* seam. A not-applicable verdict carries its reason here instead. */
|
|
50
|
+
readonly receipt: SeamEvidenceReceipt | null;
|
|
51
|
+
readonly detail: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const STAGE_RANK: Readonly<Record<SeamProofStage, number>> = {
|
|
55
|
+
claim: 0,
|
|
56
|
+
shape: 1,
|
|
57
|
+
operation: 2,
|
|
58
|
+
effect: 3,
|
|
59
|
+
'round-trip': 4,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function seamStageAtLeast(observed: SeamProofStage, required: SeamProofStage): boolean {
|
|
63
|
+
return STAGE_RANK[observed] >= STAGE_RANK[required];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Grade one seam from receipts for ONE subject epoch.
|
|
68
|
+
*
|
|
69
|
+
* The strongest stage wins. At the same stage, the last receipt wins so a
|
|
70
|
+
* successful retry can close a failure and a later failure can reopen it.
|
|
71
|
+
* Lower-stage evidence can never overrule a stronger observation: a completed
|
|
72
|
+
* cold-remount round trip is not invalidated by a later shape census.
|
|
73
|
+
*/
|
|
74
|
+
export function gradeSeamEvidence(options: {
|
|
75
|
+
readonly seam: string;
|
|
76
|
+
readonly subject: string;
|
|
77
|
+
readonly epoch: string;
|
|
78
|
+
readonly required: SeamProofStage;
|
|
79
|
+
readonly receipts: readonly SeamEvidenceReceipt[];
|
|
80
|
+
readonly notApplicable?: string | undefined;
|
|
81
|
+
}): SeamEvidenceVerdict {
|
|
82
|
+
const { seam, subject, epoch, required } = options;
|
|
83
|
+
if (options.notApplicable !== undefined) {
|
|
84
|
+
return {
|
|
85
|
+
seam,
|
|
86
|
+
subject,
|
|
87
|
+
epoch,
|
|
88
|
+
required,
|
|
89
|
+
state: 'not-applicable',
|
|
90
|
+
receipt: null,
|
|
91
|
+
detail: options.notApplicable,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let strongest: SeamEvidenceReceipt | null = null;
|
|
96
|
+
for (const receipt of options.receipts) {
|
|
97
|
+
if (receipt.seam !== seam || receipt.subject !== subject || receipt.epoch !== epoch) continue;
|
|
98
|
+
if (!strongest || STAGE_RANK[receipt.stage] >= STAGE_RANK[strongest.stage]) {
|
|
99
|
+
strongest = receipt;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!strongest) {
|
|
104
|
+
return {
|
|
105
|
+
seam,
|
|
106
|
+
subject,
|
|
107
|
+
epoch,
|
|
108
|
+
required,
|
|
109
|
+
state: 'unverified',
|
|
110
|
+
receipt: null,
|
|
111
|
+
detail: `no current-epoch evidence has reached the required ${required} stage`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (strongest.outcome === 'fail') {
|
|
115
|
+
return {
|
|
116
|
+
seam,
|
|
117
|
+
subject,
|
|
118
|
+
epoch,
|
|
119
|
+
required,
|
|
120
|
+
state: 'failed',
|
|
121
|
+
receipt: strongest,
|
|
122
|
+
detail: strongest.detail,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (strongest.outcome === 'absent') {
|
|
126
|
+
return {
|
|
127
|
+
seam,
|
|
128
|
+
subject,
|
|
129
|
+
epoch,
|
|
130
|
+
required,
|
|
131
|
+
state: 'absent',
|
|
132
|
+
receipt: strongest,
|
|
133
|
+
detail: strongest.detail,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!seamStageAtLeast(strongest.stage, required)) {
|
|
137
|
+
return {
|
|
138
|
+
seam,
|
|
139
|
+
subject,
|
|
140
|
+
epoch,
|
|
141
|
+
required,
|
|
142
|
+
state: 'unverified',
|
|
143
|
+
receipt: strongest,
|
|
144
|
+
detail: `${strongest.detail}; this proves ${strongest.stage}, but this seam requires ${required}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
seam,
|
|
149
|
+
subject,
|
|
150
|
+
epoch,
|
|
151
|
+
required,
|
|
152
|
+
state: 'verified',
|
|
153
|
+
receipt: strongest,
|
|
154
|
+
detail: strongest.detail,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** In-memory current-session ledger. It is evidence, not a project file and
|
|
159
|
+
* therefore introduces no Volter data format. */
|
|
160
|
+
export class SeamEvidenceLedger {
|
|
161
|
+
readonly #receipts: SeamEvidenceReceipt[] = [];
|
|
162
|
+
|
|
163
|
+
record(receipt: SeamEvidenceReceipt): void {
|
|
164
|
+
if (!receipt.seam || !receipt.subject || !receipt.epoch || !receipt.detail.trim()) {
|
|
165
|
+
throw new Error('seam evidence requires non-empty seam, subject, epoch, and detail');
|
|
166
|
+
}
|
|
167
|
+
this.#receipts.push(receipt);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
receipts(options?: {
|
|
171
|
+
readonly subject?: string | undefined;
|
|
172
|
+
readonly epoch?: string | undefined;
|
|
173
|
+
}): readonly SeamEvidenceReceipt[] {
|
|
174
|
+
return this.#receipts.filter(
|
|
175
|
+
(receipt) =>
|
|
176
|
+
(options?.subject === undefined || receipt.subject === options.subject) &&
|
|
177
|
+
(options?.epoch === undefined || receipt.epoch === options.epoch),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
verdict(options: {
|
|
182
|
+
readonly seam: string;
|
|
183
|
+
readonly subject: string;
|
|
184
|
+
readonly epoch: string;
|
|
185
|
+
readonly required: SeamProofStage;
|
|
186
|
+
readonly notApplicable?: string | undefined;
|
|
187
|
+
}): SeamEvidenceVerdict {
|
|
188
|
+
return gradeSeamEvidence({ ...options, receipts: this.#receipts });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
clearSubject(subject: string): void {
|
|
192
|
+
for (let index = this.#receipts.length - 1; index >= 0; index -= 1) {
|
|
193
|
+
if (this.#receipts[index]?.subject === subject) this.#receipts.splice(index, 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clear(): void {
|
|
198
|
+
this.#receipts.length = 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type NonUndefined<T> = Exclude<T, undefined>;
|
|
203
|
+
type MemberKind<T> = NonUndefined<T> extends (...args: never[]) => unknown ? 'function' : 'value';
|
|
204
|
+
|
|
205
|
+
/** A compiler-pinned description of every member of `T`. Adding, removing, or
|
|
206
|
+
* changing the optionality/kind of a contract member breaks its descriptor. */
|
|
207
|
+
export type SeamShape<T> = {
|
|
208
|
+
readonly [K in keyof T]-?: {
|
|
209
|
+
readonly optional: undefined extends T[K] ? true : false;
|
|
210
|
+
readonly kind: MemberKind<T[K]>;
|
|
211
|
+
readonly required: SeamProofStage;
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export function defineSeamShape<T>() {
|
|
216
|
+
return <const TShape extends SeamShape<T>>(shape: TShape): TShape => shape;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function memberShapeReceipt(options: {
|
|
220
|
+
readonly seam: string;
|
|
221
|
+
readonly value: unknown;
|
|
222
|
+
readonly optional: boolean;
|
|
223
|
+
readonly kind: 'function' | 'value';
|
|
224
|
+
readonly subject: string;
|
|
225
|
+
readonly epoch: string;
|
|
226
|
+
}): SeamEvidenceReceipt {
|
|
227
|
+
if (options.value === undefined) {
|
|
228
|
+
return {
|
|
229
|
+
seam: options.seam,
|
|
230
|
+
subject: options.subject,
|
|
231
|
+
epoch: options.epoch,
|
|
232
|
+
stage: 'shape',
|
|
233
|
+
outcome: options.optional ? 'absent' : 'fail',
|
|
234
|
+
source: 'shape-check',
|
|
235
|
+
detail: options.optional
|
|
236
|
+
? `${options.seam} is not exposed by this subject`
|
|
237
|
+
: `${options.seam} is required by the contract but is absent`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const actual = typeof options.value;
|
|
241
|
+
const valid = options.kind === 'function' ? actual === 'function' : actual !== 'function';
|
|
242
|
+
return {
|
|
243
|
+
seam: options.seam,
|
|
244
|
+
subject: options.subject,
|
|
245
|
+
epoch: options.epoch,
|
|
246
|
+
stage: 'shape',
|
|
247
|
+
outcome: valid ? 'pass' : 'fail',
|
|
248
|
+
source: 'shape-check',
|
|
249
|
+
detail: valid
|
|
250
|
+
? `${options.seam} has the contract's ${options.kind} shape`
|
|
251
|
+
: `${options.seam} is ${actual}; the contract requires a ${options.kind}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Shape is intentionally only shape. Successful members produce `shape`
|
|
256
|
+
* receipts; callers must add operation/effect/round-trip receipts from actual
|
|
257
|
+
* consumers or conformance probes before those seams can verify. */
|
|
258
|
+
export function inspectSeamShape<T extends object>(options: {
|
|
259
|
+
readonly prefix: string;
|
|
260
|
+
readonly value: unknown;
|
|
261
|
+
readonly shape: SeamShape<T>;
|
|
262
|
+
readonly subject: string;
|
|
263
|
+
readonly epoch: string;
|
|
264
|
+
}): readonly SeamEvidenceReceipt[] {
|
|
265
|
+
const receipts: SeamEvidenceReceipt[] = [];
|
|
266
|
+
const object =
|
|
267
|
+
options.value !== null &&
|
|
268
|
+
(typeof options.value === 'object' || typeof options.value === 'function')
|
|
269
|
+
? (options.value as Record<string, unknown>)
|
|
270
|
+
: null;
|
|
271
|
+
const members = Object.entries(options.shape) as Array<
|
|
272
|
+
[string, { readonly optional: boolean; readonly kind: 'function' | 'value' }]
|
|
273
|
+
>;
|
|
274
|
+
for (const [member, spec] of members) {
|
|
275
|
+
const seam = `${options.prefix}.${member}`;
|
|
276
|
+
const value = object?.[member];
|
|
277
|
+
receipts.push(
|
|
278
|
+
memberShapeReceipt({
|
|
279
|
+
seam,
|
|
280
|
+
subject: options.subject,
|
|
281
|
+
epoch: options.epoch,
|
|
282
|
+
value,
|
|
283
|
+
optional: spec.optional,
|
|
284
|
+
kind: spec.kind,
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return receipts;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Grade a carrier and every member it actually advertises.
|
|
293
|
+
*
|
|
294
|
+
* Optional-and-absent is honest degradation. Optional-and-present is a real
|
|
295
|
+
* promise and is held to exactly the same proof bar as a required member. This
|
|
296
|
+
* is the shared rule that prevents each coverage family from inventing a
|
|
297
|
+
* different meaning for “provider present”.
|
|
298
|
+
*/
|
|
299
|
+
export function gradeSeamCarrier(options: {
|
|
300
|
+
readonly seam: string;
|
|
301
|
+
readonly required: SeamProofStage;
|
|
302
|
+
readonly shape?: SeamShape<object> | undefined;
|
|
303
|
+
readonly subject: string;
|
|
304
|
+
readonly epoch: string;
|
|
305
|
+
readonly receipts: readonly SeamEvidenceReceipt[];
|
|
306
|
+
}): SeamEvidenceVerdict {
|
|
307
|
+
const base = gradeSeamEvidence(options);
|
|
308
|
+
if (base.state === 'absent' || base.state === 'failed' || !options.shape) return base;
|
|
309
|
+
|
|
310
|
+
for (const [member, spec] of Object.entries(options.shape)) {
|
|
311
|
+
const memberVerdict = gradeSeamEvidence({
|
|
312
|
+
seam: `${options.seam}.${member}`,
|
|
313
|
+
subject: options.subject,
|
|
314
|
+
epoch: options.epoch,
|
|
315
|
+
required: spec.required,
|
|
316
|
+
receipts: options.receipts,
|
|
317
|
+
});
|
|
318
|
+
if (spec.optional && memberVerdict.state === 'absent') continue;
|
|
319
|
+
if (memberVerdict.state === 'failed' || memberVerdict.state === 'absent') {
|
|
320
|
+
return {
|
|
321
|
+
...base,
|
|
322
|
+
state: 'failed',
|
|
323
|
+
receipt: memberVerdict.receipt,
|
|
324
|
+
detail: `${options.seam} is malformed: ${memberVerdict.detail}`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (memberVerdict.state !== 'verified') {
|
|
328
|
+
return {
|
|
329
|
+
...base,
|
|
330
|
+
state: 'unverified',
|
|
331
|
+
detail: `${options.seam} has the required shape, but ${memberVerdict.seam} is unverified: ${memberVerdict.detail}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return base;
|
|
336
|
+
}
|