@unrulysystems/native-motion-conformance 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +66 -0
  4. package/package.json +33 -0
  5. package/src/adapter.ts +42 -0
  6. package/src/adapters/motion-dom.ts +96 -0
  7. package/src/adapters/native.ts +95 -0
  8. package/src/authoring.ts +78 -0
  9. package/src/comparator.ts +129 -0
  10. package/src/config.ts +22 -0
  11. package/src/declarations.ts +21 -0
  12. package/src/index.ts +144 -0
  13. package/src/oracle/attestation.ts +100 -0
  14. package/src/oracle/constants.ts +24 -0
  15. package/src/oracle/controls.ts +202 -0
  16. package/src/oracle/errors.ts +12 -0
  17. package/src/oracle/exportTrace.ts +134 -0
  18. package/src/oracle/index.ts +133 -0
  19. package/src/oracle/judge.ts +1374 -0
  20. package/src/oracle/presenter.ts +372 -0
  21. package/src/oracle/runRecord.ts +307 -0
  22. package/src/oracle/scenarios.ts +115 -0
  23. package/src/oracle/scripts/gesture.ts +218 -0
  24. package/src/oracle/serialize.ts +91 -0
  25. package/src/oracle/sweep.ts +155 -0
  26. package/src/oracle/types.ts +76 -0
  27. package/src/oracle/velocity.ts +44 -0
  28. package/src/parity.ts +136 -0
  29. package/src/runner.ts +168 -0
  30. package/src/scenario.ts +179 -0
  31. package/src/scenarios/appstore-choreography.ts +105 -0
  32. package/src/scenarios/component.ts +516 -0
  33. package/src/scenarios/driver.ts +322 -0
  34. package/src/scenarios/gesture.ts +363 -0
  35. package/src/scenarios/layout-identity.ts +264 -0
  36. package/src/scenarios/layout.ts +258 -0
  37. package/src/scenarios/presence.ts +302 -0
  38. package/src/scenarios/spring.ts +180 -0
  39. package/src/scenarios/value-types.ts +107 -0
  40. package/src/suite.ts +44 -0
package/src/index.ts ADDED
@@ -0,0 +1,144 @@
1
+ // Public surface of the conformance harness: the engine-agnostic scenario format, the two engine
2
+ // adapters, the runner, and the pure cross-engine comparator. No host dependencies leak here — the
3
+ // package's only engine imports are the core (native, under test) and pinned motion-dom (the oracle).
4
+
5
+ export type {
6
+ Capability,
7
+ MotionScalar,
8
+ Scenario,
9
+ SpringKind,
10
+ SpringScenario,
11
+ SpringTrajectorySpec,
12
+ TimelineStep,
13
+ ToleranceBand,
14
+ ValueTypeScenario,
15
+ } from './scenario'
16
+ export { CORE_SCENARIOS } from './scenario'
17
+ export { SPRING_SCENARIOS } from './scenarios/spring'
18
+ export { VALUE_TYPE_SCENARIOS } from './scenarios/value-types'
19
+ export type { EngineAdapter, EngineValue, TrajectoryPoint } from './adapter'
20
+ export { nativeAdapter } from './adapters/native'
21
+ export { motionDomAdapter } from './adapters/motion-dom'
22
+ export type { Reading, ScenarioResult, Verdict } from './runner'
23
+ export { runScenario, runSpringScenario, runValueTypeScenario, scalarWithin } from './runner'
24
+ export type { CrossEngineMismatch, CrossEngineResult } from './comparator'
25
+ export { compareEngines, compareValueTypeSeries } from './comparator'
26
+ export type { SuiteResult } from './suite'
27
+ export { aggregateSuite } from './suite'
28
+ export { DEFAULT_BAND, SPRING_BAND } from './config'
29
+
30
+ // SPEC-UNIVERSAL-SUBSET authoring gate (REQ-SUBSET-018): the registry as the scenario capability
31
+ // whitelist + web-altitude routing. Rejects unknown/off-host capabilities at author time (fail-closed).
32
+ export type { AuthoredScenario, AuthoredTargets, WebAltitude } from './authoring'
33
+ export { authorScenario, webAltitudeFor } from './authoring'
34
+
35
+ // SPEC-COMPONENT Altitude-2 boundary scenarios (REQ-API-001/002/005/006): first-paint, initial={false},
36
+ // partial-hold, and fail-loud validation, run against the fake host at M1 and both engines at M2.
37
+ // EXCEPTIONS — CMP-R7/CMP-R8/CMP-R9-cross-engine-parity (REQ-API-032/033/034, REQ-CONFORM-002):
38
+ // parity is BETWEEN engines, so no single host asserts it. All three rows stay permanently
39
+ // `unassertable`: R7 is discharged by variants.e2e.ts + PROOF.md §R7; R8 by keyframes.e2e.ts +
40
+ // PROOF.md §R8 device keyframes card; R9 by examples.smoke/text-layout/image-gallery e2e
41
+ // (Chromium 22/22) + PROOF.md §R9 device element cards. None flip to an in-suite pass
42
+ // (R7 M5, ratified Option A — SPEC-CONFORMANCE §3).
43
+ export type { ComponentScenario } from './scenarios/component'
44
+ export { COMPONENT_SCENARIOS, runComponentScenario } from './scenarios/component'
45
+
46
+ export type { DriverScenario } from './scenarios/driver'
47
+ export { DRIVER_SCENARIOS, runDriverScenario } from './scenarios/driver'
48
+
49
+ export type { PresenceScenario } from './scenarios/presence'
50
+ export { PRESENCE_SCENARIOS, runPresenceScenario } from './scenarios/presence'
51
+
52
+ export type { GestureScenario } from './scenarios/gesture'
53
+ export { GESTURE_SCENARIOS, runGestureScenario } from './scenarios/gesture'
54
+
55
+ export type { LayoutScenario } from './scenarios/layout'
56
+ export { LAYOUT_SCENARIOS, runLayoutScenario } from './scenarios/layout'
57
+
58
+ export type { LayoutIdentityScenario } from './scenarios/layout-identity'
59
+ export { LAYOUT_IDENTITY_SCENARIOS, runLayoutIdentityScenario } from './scenarios/layout-identity'
60
+
61
+ // Cross-engine parity seam (REQ-CONFORM-002, additive): lets a real second engine OWN the
62
+ // `*.cross-engine-parity` scenarios via the aggregator's ownership rule. The web engine is
63
+ // injected as a narrow interface; the comparison and band live here (REQ-WEB-017).
64
+ export type { GestureParityCase, GestureWebEngine } from './parity'
65
+ export {
66
+ GESTURE_ENDPOINT_BAND,
67
+ GESTURE_PARITY_CASES,
68
+ GESTURE_PARITY_SCENARIO_ID,
69
+ LAYOUT_PARITY_COUNTERPART,
70
+ LAYOUT_PARITY_SCENARIO_ID,
71
+ referenceReleaseEndpoint,
72
+ LAYOUT_IDENTITY_PARITY_COUNTERPART,
73
+ runGestureParityScenario,
74
+ } from './parity'
75
+ export type { AppstoreChoreographyScenario } from './scenarios/appstore-choreography'
76
+ export {
77
+ APPSTORE_CHOREOGRAPHY_SCENARIOS,
78
+ runAppstoreChoreographyScenario,
79
+ } from './scenarios/appstore-choreography'
80
+
81
+ // M2.7 oracle M1 trajectory exporter (REQ-ORACLE-004) — minimal surface for phases 2–4 + tests.
82
+ export type {
83
+ OracleEngine,
84
+ OracleRealTrace,
85
+ OracleTrace,
86
+ SubjectiveSliceId,
87
+ TraceEvent,
88
+ TraceEventKind,
89
+ TraceRow,
90
+ VelocityProvenance,
91
+ } from './oracle'
92
+ export {
93
+ SUBJECTIVE_SLICE_IDS,
94
+ ORACLE_FRAME_MS,
95
+ createJudgeBundle,
96
+ exportTrace,
97
+ exportTraceRun,
98
+ serializeTrace,
99
+ serializeJudgeBundle,
100
+ isSubjectiveSliceId,
101
+ } from './oracle'
102
+ export type {
103
+ OracleJudgeBundle,
104
+ OraclePresentation,
105
+ OracleUnblindingEntry,
106
+ OracleUnblindingRecord,
107
+ } from './oracle'
108
+ export type { OracleTraceAttestationRegistry, OracleTraceRun } from './oracle'
109
+ export type {
110
+ JudgeDispatchOutcome,
111
+ JudgeVerdictOutcome,
112
+ JudgeProvider,
113
+ JudgeProviderConfig,
114
+ OracleAdvisoryOrdering,
115
+ OracleGuardInput,
116
+ OracleGuardOutcome,
117
+ OracleJudgeDimension,
118
+ OracleJudgeDimensionVerdict,
119
+ OracleJudgeIssue,
120
+ OracleJudgeValue,
121
+ OracleJudgeVerdict,
122
+ OracleUnclassifiedFinding,
123
+ OracleRunBundleInput,
124
+ OracleRunBundleRecord,
125
+ OracleRunInputs,
126
+ OracleRunRecord,
127
+ OracleRunRecordFile,
128
+ } from './oracle'
129
+ export {
130
+ buildOracleRunRecord,
131
+ createCliJudgeProvider,
132
+ createIsolatedJudgeSpawnOptions,
133
+ DEFAULT_JUDGE_PROVIDER_CONFIG,
134
+ decideOracleGuard,
135
+ dispatchJudge,
136
+ judgeBundle,
137
+ OracleJudgeError,
138
+ ORACLE_JUDGE_DIMENSIONS,
139
+ ORACLE_JUDGE_VALUES,
140
+ ORACLE_RUNS_DIRECTORY,
141
+ OracleRunRecordError,
142
+ parseJudgeVerdict,
143
+ serializeOracleRunRecord,
144
+ } from './oracle'
@@ -0,0 +1,100 @@
1
+ // Exporter-issued trace attestations. A registry is created for one run and passed with that run;
2
+ // no process-global registry can accidentally admit a trace from a different run.
3
+
4
+ import { OracleTraceError } from './errors'
5
+ import { serializeTrace } from './serialize'
6
+ import type { OracleRealTrace, OracleTrace } from './types'
7
+
8
+ const traceAttestation = Symbol('oracle trace attestation')
9
+
10
+ type TraceAttestation = {
11
+ readonly registry: OracleTraceAttestationRegistry
12
+ readonly hash: string
13
+ }
14
+
15
+ export interface OracleTraceAttestationRegistry {
16
+ /** The content hashes issued by the exporter during this run, in deterministic insertion order. */
17
+ readonly traceHashes: readonly string[]
18
+ }
19
+
20
+ class RunTraceAttestationRegistry implements OracleTraceAttestationRegistry {
21
+ readonly #hashes = new Set<string>()
22
+
23
+ get traceHashes(): readonly string[] {
24
+ return [...this.#hashes]
25
+ }
26
+
27
+ register(hash: string): void {
28
+ this.#hashes.add(hash)
29
+ }
30
+
31
+ contains(hash: string): boolean {
32
+ return this.#hashes.has(hash)
33
+ }
34
+ }
35
+
36
+ /** A fixed 64-bit FNV-1a content hash, deliberately host-crypto independent. */
37
+ export function oracleContentHash(content: string): string {
38
+ let hash = 0xcbf29ce484222325n
39
+ for (let index = 0; index < content.length; index += 1) {
40
+ hash ^= BigInt(content.charCodeAt(index))
41
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n)
42
+ }
43
+ return hash.toString(16).padStart(16, '0')
44
+ }
45
+
46
+ export function createOracleTraceAttestationRegistry(): OracleTraceAttestationRegistry {
47
+ return new RunTraceAttestationRegistry()
48
+ }
49
+
50
+ function mutableRegistry(registry: OracleTraceAttestationRegistry): RunTraceAttestationRegistry {
51
+ if (!(registry instanceof RunTraceAttestationRegistry)) {
52
+ throw new OracleTraceError('attestation', 'registry was not issued by the oracle exporter')
53
+ }
54
+ return registry
55
+ }
56
+
57
+ function attestationOf(trace: OracleTrace): TraceAttestation | undefined {
58
+ return (trace as OracleTrace & { readonly [traceAttestation]?: TraceAttestation })[
59
+ traceAttestation
60
+ ]
61
+ }
62
+
63
+ /** Registers exactly the canonical bytes the exporter produced and retains that ticket privately. */
64
+ export function attestExportedTrace(
65
+ trace: OracleTrace,
66
+ registry: OracleTraceAttestationRegistry,
67
+ ): OracleRealTrace {
68
+ const hash = oracleContentHash(serializeTrace(trace))
69
+ mutableRegistry(registry).register(hash)
70
+ Object.defineProperty(trace, traceAttestation, {
71
+ configurable: false,
72
+ enumerable: false,
73
+ value: { registry, hash } satisfies TraceAttestation,
74
+ writable: false,
75
+ })
76
+ return trace as OracleRealTrace
77
+ }
78
+
79
+ /**
80
+ * Requires the exact exporter-issued trace, not merely an object shaped like one. Spreads and
81
+ * field stripping intentionally discard the private ticket, so a degraded control cannot re-enter
82
+ * as a real candidate.
83
+ */
84
+ export function assertAttestedRealTrace(
85
+ trace: OracleTrace,
86
+ context: string,
87
+ ): asserts trace is OracleRealTrace {
88
+ const attestation = attestationOf(trace)
89
+ if (attestation === undefined) {
90
+ throw new OracleTraceError(trace.scenarioId, `${context}: trace has no exporter attestation`)
91
+ }
92
+ const registry = mutableRegistry(attestation.registry)
93
+ const actualHash = oracleContentHash(serializeTrace(trace))
94
+ if (actualHash !== attestation.hash || !registry.contains(actualHash)) {
95
+ throw new OracleTraceError(
96
+ trace.scenarioId,
97
+ `${context}: trace does not match its exporter attestation`,
98
+ )
99
+ }
100
+ }
@@ -0,0 +1,24 @@
1
+ // Ratified M1 calibration-control constants (M2.7 A5). These are seeded once so oracle
2
+ // calibration stays stable; callers must not tune a defect to make a particular run pass.
3
+
4
+ /** M1's position-continuity tolerance; C-DISC is deliberately 48 times larger than this. */
5
+ export const ORACLE_CONTROL_POSITION_EPSILON_PX = 0.5
6
+
7
+ /** C-DISC changes every value at/after its marked boundary by this visible distance. */
8
+ export const ORACLE_CONTROL_DISCONTINUITY_PX = ORACLE_CONTROL_POSITION_EPSILON_PX * 48
9
+
10
+ /** C-DISC makes the boundary's velocity discontinuity explicit rather than preserving a handoff. */
11
+ export const ORACLE_CONTROL_VELOCITY_RESET = 0
12
+
13
+ /** C-DISC plants a mid-settle step here when a pure settle has no release or interrupt mark. */
14
+ export const ORACLE_CONTROL_DISC_SETTLE_FRACTION = 0.25
15
+
16
+ /** Trace timestamps are milliseconds while the public velocity unit is value-units per second. */
17
+ export const ORACLE_CONTROL_MILLISECONDS_PER_SECOND = 1000
18
+
19
+ /** C-LIN replaces precisely the final boundary-to-end settle segment with an endpoint ramp. */
20
+ export const ORACLE_CONTROL_LINEAR_RAMP_POLICY = {
21
+ start: 'final-boundary',
22
+ end: 'trace-end',
23
+ velocity: 'constant-endpoint-slope',
24
+ } as const
@@ -0,0 +1,202 @@
1
+ // M1 planted-defect controls (REQ-ORACLE-006). These are pure trace transforms: the oracle
2
+ // consumes an already-exported real trace and never imports or drives an animation engine.
3
+
4
+ import {
5
+ ORACLE_CONTROL_DISC_SETTLE_FRACTION,
6
+ ORACLE_CONTROL_DISCONTINUITY_PX,
7
+ ORACLE_CONTROL_MILLISECONDS_PER_SECOND,
8
+ ORACLE_CONTROL_VELOCITY_RESET,
9
+ } from './constants'
10
+ import { OracleTraceError } from './errors'
11
+ import { assertAttestedRealTrace } from './attestation'
12
+ import { serializeTrace } from './serialize'
13
+ import type { OracleRealTrace, OracleTrace, TraceEvent, TraceRow } from './types'
14
+
15
+ export const ORACLE_CONTROL_KINDS = ['C-LIN', 'C-DISC'] as const
16
+ export type OracleControlKind = (typeof ORACLE_CONTROL_KINDS)[number]
17
+
18
+ export function isOracleControlKind(kind: string): kind is OracleControlKind {
19
+ return (ORACLE_CONTROL_KINDS as readonly string[]).includes(kind)
20
+ }
21
+
22
+ /** Internal provenance the phase-3 presenter must remove before it constructs a blinded trace. */
23
+ export interface OracleControlMetadata {
24
+ readonly kind: OracleControlKind
25
+ readonly derivedFrom: Pick<OracleTrace, 'scenarioId' | 'engine' | 'grid'>
26
+ }
27
+
28
+ /** A real trace plus its internal-only planted-defect provenance. */
29
+ export type OracleControlTrace = OracleTrace & {
30
+ readonly control: OracleControlMetadata
31
+ }
32
+
33
+ /**
34
+ * The phase-3 blind bundle surface. `never` makes an accidental metadata carry impossible at the
35
+ * type boundary: a control must be copied into a fresh presenter-owned object before dispatch.
36
+ */
37
+ export interface BlindedOracleTrace {
38
+ readonly label: string
39
+ readonly grid: OracleTrace['grid']
40
+ readonly rows: readonly TraceRow[]
41
+ readonly eventTimes: readonly TraceEvent[]
42
+ readonly scenarioId?: never
43
+ readonly velocityProvenance?: never
44
+ readonly engine?: never
45
+ readonly control?: never
46
+ }
47
+
48
+ type LinearControlSegment = {
49
+ readonly start: number
50
+ readonly startRow: TraceRow
51
+ readonly endRow: TraceRow
52
+ readonly durationMs: number
53
+ }
54
+
55
+ function controlError(
56
+ trace: OracleTrace,
57
+ kind: OracleControlKind,
58
+ message: string,
59
+ ): OracleTraceError {
60
+ return new OracleTraceError(trace.scenarioId, `${kind}: ${message}`)
61
+ }
62
+
63
+ function findFinalBoundary(trace: OracleTrace): TraceEvent | undefined {
64
+ for (let index = trace.eventTimes.length - 1; index >= 0; index -= 1) {
65
+ const event = trace.eventTimes[index]
66
+ if (
67
+ event?.kind === 'animation-start' ||
68
+ event?.kind === 'release' ||
69
+ event?.kind === 'interrupt'
70
+ ) {
71
+ return event
72
+ }
73
+ }
74
+ return undefined
75
+ }
76
+
77
+ /** Returns the final control boundary's row, or undefined when the trace cannot support controls. */
78
+ function finalBoundaryIndex(trace: OracleTrace): number | undefined {
79
+ const boundary = findFinalBoundary(trace)
80
+ if (boundary === undefined) return undefined
81
+ const index = trace.rows.findIndex((row) => row.t === boundary.t)
82
+ return index < 0 ? undefined : index
83
+ }
84
+
85
+ /** The exact C-LIN precondition, shared by eligibility selection and injection. */
86
+ function linearControlSegment(trace: OracleTrace): LinearControlSegment | undefined {
87
+ const start = finalBoundaryIndex(trace)
88
+ if (start === undefined || trace.rows.length - start < 3) return undefined
89
+ const startRow = trace.rows[start]
90
+ const endRow = trace.rows.at(-1)
91
+ if (startRow === undefined || endRow === undefined) return undefined
92
+ const durationMs = endRow.t - startRow.t
93
+ if (durationMs <= 0) return undefined
94
+ return { start, startRow, endRow, durationMs }
95
+ }
96
+
97
+ /** A gesture seam takes precedence; pure settles receive the ratified 25%-of-settle defect. */
98
+ function discontinuityControlIndex(trace: OracleTrace): number | undefined {
99
+ for (let index = trace.eventTimes.length - 1; index >= 0; index -= 1) {
100
+ const event = trace.eventTimes[index]
101
+ if (event?.kind === 'release' || event?.kind === 'interrupt') {
102
+ const mark = trace.rows.findIndex((row) => row.t === event.t)
103
+ return mark < 1 ? undefined : mark
104
+ }
105
+ }
106
+
107
+ const start = finalBoundaryIndex(trace)
108
+ if (start === undefined) return undefined
109
+ const end = trace.rows.length - 1
110
+ const index = Math.round(start + (end - start) * ORACLE_CONTROL_DISC_SETTLE_FRACTION)
111
+ if (index <= start || index > end) {
112
+ return undefined
113
+ }
114
+ return index
115
+ }
116
+
117
+ /**
118
+ * Derives the controls a frozen trace can actually support. The presenter must use this exact
119
+ * predicate instead of assuming every calibration control applies to every scenario slice.
120
+ */
121
+ export function eligibleOracleControlKinds(trace: OracleTrace): readonly OracleControlKind[] {
122
+ const kinds: OracleControlKind[] = []
123
+ if (discontinuityControlIndex(trace) !== undefined) kinds.push('C-DISC')
124
+ if (linearControlSegment(trace) !== undefined) kinds.push('C-LIN')
125
+ return kinds
126
+ }
127
+
128
+ function controlMetadata(trace: OracleTrace, kind: OracleControlKind): OracleControlMetadata {
129
+ return {
130
+ kind,
131
+ derivedFrom: { scenarioId: trace.scenarioId, engine: trace.engine, grid: trace.grid },
132
+ }
133
+ }
134
+
135
+ /** Casts and field stripping cannot make a planted defect a real source; attestation is the gate. */
136
+ function assertRealTraceInput(trace: OracleRealTrace, kind: OracleControlKind): void {
137
+ assertAttestedRealTrace(trace, `${kind} control source`)
138
+ }
139
+
140
+ /**
141
+ * Replaces the final boundary-to-end segment with a constant-velocity endpoint ramp.
142
+ * Values at both endpoints are deliberately assigned directly, not recomputed, so they remain
143
+ * exactly equal to the real trace despite floating-point interpolation rounding.
144
+ */
145
+ export function injectLinearControl(trace: OracleRealTrace): OracleControlTrace {
146
+ assertRealTraceInput(trace, 'C-LIN')
147
+ // Reuse phase 1's fail-closed numeric validation before deriving a control from these samples.
148
+ serializeTrace(trace)
149
+ const segment = linearControlSegment(trace)
150
+ if (segment === undefined) {
151
+ throw controlError(trace, 'C-LIN', 'settle segment is too short for a linear ramp')
152
+ }
153
+ const { start, startRow, endRow, durationMs } = segment
154
+ const velocity =
155
+ ((endRow.value - startRow.value) / durationMs) * ORACLE_CONTROL_MILLISECONDS_PER_SECOND
156
+
157
+ return {
158
+ ...trace,
159
+ rows: trace.rows.map((row, index) => {
160
+ if (index < start) return row
161
+ if (index === start) return { ...row, value: startRow.value, velocity }
162
+ if (index === trace.rows.length - 1) return { ...row, value: endRow.value, velocity }
163
+ const progress = (row.t - startRow.t) / durationMs
164
+ return {
165
+ ...row,
166
+ value: startRow.value + (endRow.value - startRow.value) * progress,
167
+ velocity,
168
+ }
169
+ }),
170
+ control: controlMetadata(trace, 'C-LIN'),
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Plants a visible position step at the final release/interrupt mark. A gesture-less pure settle
176
+ * gets the same defect at its ratified fractional grid row. The mark receives the velocity reset;
177
+ * subsequent rows retain their observed velocity so the control differs only at the planted seam.
178
+ */
179
+ export function injectDiscontinuityControl(trace: OracleRealTrace): OracleControlTrace {
180
+ assertRealTraceInput(trace, 'C-DISC')
181
+ serializeTrace(trace)
182
+ const mark = discontinuityControlIndex(trace)
183
+ if (mark === undefined) {
184
+ throw controlError(trace, 'C-DISC', 'requires a usable boundary mark and prior trace row')
185
+ }
186
+
187
+ return {
188
+ ...trace,
189
+ rows: trace.rows.map((row, index) => {
190
+ if (index < mark) return row
191
+ if (index === mark) {
192
+ return {
193
+ ...row,
194
+ value: row.value + ORACLE_CONTROL_DISCONTINUITY_PX,
195
+ velocity: ORACLE_CONTROL_VELOCITY_RESET,
196
+ }
197
+ }
198
+ return { ...row, value: row.value + ORACLE_CONTROL_DISCONTINUITY_PX }
199
+ }),
200
+ control: controlMetadata(trace, 'C-DISC'),
201
+ }
202
+ }
@@ -0,0 +1,12 @@
1
+ // Typed fail-loud errors for the oracle trace exporter. Every throw names the slice so a
2
+ // budget overrun or empty sweep is never a silent empty artifact.
3
+
4
+ export class OracleTraceError extends Error {
5
+ readonly sliceId: string
6
+
7
+ constructor(sliceId: string, message: string) {
8
+ super(`oracle trace [${sliceId}]: ${message}`)
9
+ this.name = 'OracleTraceError'
10
+ this.sliceId = sliceId
11
+ }
12
+ }
@@ -0,0 +1,134 @@
1
+ // Public entry: export a frame-swept oracle trace for one subjective slice (M2.7 phase 1 / A1).
2
+ // The five gesture slices are native-only. SHEET-01 joins both engines through the existing dense
3
+ // spring-trajectory adapter seam; no new host gesture machinery is introduced here.
4
+
5
+ import { OracleTraceError } from './errors'
6
+ import {
7
+ attestExportedTrace,
8
+ createOracleTraceAttestationRegistry,
9
+ type OracleTraceAttestationRegistry,
10
+ } from './attestation'
11
+ import { exportGestureTrace, isGestureSliceId } from './scripts/gesture'
12
+ import { nativeAdapter } from '../adapters/native'
13
+ import { motionDomAdapter } from '../adapters/motion-dom'
14
+ import { SPRING_SCENARIOS } from '../scenarios/spring'
15
+ import {
16
+ isSubjectiveSliceId,
17
+ isOracleEngine,
18
+ ORACLE_ENGINES,
19
+ type OracleEngine,
20
+ type OracleRealTrace,
21
+ type OracleTrace,
22
+ type SubjectiveSliceId,
23
+ } from './types'
24
+
25
+ const SHEET_01_SLICE_ID = 'sheet.open-to-snap'
26
+ const SHEET_01_TRAJECTORY = SPRING_SCENARIOS.find((scenario) => scenario.id === 'SPRING-critical')
27
+ function exportSheet01SpringTrace(engine: OracleEngine): OracleTrace {
28
+ const scenario = SHEET_01_TRAJECTORY
29
+ if (scenario === undefined) {
30
+ throw new OracleTraceError(SHEET_01_SLICE_ID, 'missing reused SPRING-critical trajectory spec')
31
+ }
32
+ const adapter = engine === 'native-core' ? nativeAdapter() : motionDomAdapter()
33
+ if (adapter.sampleTrajectory === undefined) {
34
+ throw new OracleTraceError(SHEET_01_SLICE_ID, `${engine} has no spring-trajectory seam`)
35
+ }
36
+ const samples = adapter.sampleTrajectory(scenario.kind, scenario.spec, scenario.tGrid)
37
+ const firstSample = samples[0]
38
+ if (firstSample === undefined) {
39
+ throw new OracleTraceError(SHEET_01_SLICE_ID, `${engine} returned an empty spring trajectory`)
40
+ }
41
+ return {
42
+ scenarioId: SHEET_01_SLICE_ID,
43
+ engine,
44
+ grid: 'fixed-60fps',
45
+ rows: samples.map(({ t, value, velocity }) => ({ t, value, velocity })),
46
+ // A gesture-less spring has no release/interrupt seam. Mark its animation origin explicitly
47
+ // so the calibration controls share the same boundary vocabulary as gesture traces.
48
+ eventTimes: [{ t: firstSample.t, kind: 'animation-start' }],
49
+ velocityProvenance: 'engine',
50
+ }
51
+ }
52
+
53
+ function exportTraceInto(
54
+ scenarioId: SubjectiveSliceId | string,
55
+ registry: OracleTraceAttestationRegistry,
56
+ engine: OracleEngine = 'native-core',
57
+ ): OracleRealTrace {
58
+ if (!isSubjectiveSliceId(scenarioId)) {
59
+ throw new OracleTraceError(String(scenarioId), `unknown subjective slice id`)
60
+ }
61
+
62
+ if (!isOracleEngine(engine)) {
63
+ throw new OracleTraceError(
64
+ scenarioId,
65
+ `unsupported engine '${String(engine)}'; allowed: ${ORACLE_ENGINES.join(', ')}`,
66
+ )
67
+ }
68
+
69
+ if (scenarioId === SHEET_01_SLICE_ID) {
70
+ return attestExportedTrace(exportSheet01SpringTrace(engine), registry)
71
+ }
72
+
73
+ if (engine === 'motion-dom') {
74
+ // Dense web gesture sweeps would require forbidden new machinery; only SHEET-01 has its seam.
75
+ throw new OracleTraceError(
76
+ scenarioId,
77
+ `motion-dom trace deferred — no deterministic M1 frame-sweep seam for this gesture slice`,
78
+ )
79
+ }
80
+
81
+ if (engine !== 'native-core') {
82
+ throw new OracleTraceError(scenarioId, `unsupported engine '${String(engine)}'`)
83
+ }
84
+
85
+ if (isGestureSliceId(scenarioId)) {
86
+ const { rows, eventTimes, velocityProvenance } = exportGestureTrace(scenarioId)
87
+ return attestExportedTrace(
88
+ {
89
+ scenarioId,
90
+ engine: 'native-core',
91
+ grid: 'fixed-60fps',
92
+ rows,
93
+ eventTimes,
94
+ velocityProvenance,
95
+ },
96
+ registry,
97
+ )
98
+ }
99
+
100
+ throw new OracleTraceError(scenarioId, 'slice registered but no exporter branch')
101
+ }
102
+
103
+ /** Exports one trace in its own run-scoped registry for focused callers and unit tests. */
104
+ export function exportTrace(
105
+ scenarioId: SubjectiveSliceId | string,
106
+ engine: OracleEngine = 'native-core',
107
+ ): OracleRealTrace {
108
+ return exportTraceInto(scenarioId, createOracleTraceAttestationRegistry(), engine)
109
+ }
110
+
111
+ export interface OracleTraceRun {
112
+ readonly realTraces: readonly OracleRealTrace[]
113
+ readonly attestations: OracleTraceAttestationRegistry
114
+ }
115
+
116
+ /**
117
+ * Exports every candidate for a judge run into one registry. Gesture slices have their available
118
+ * native trace; SHEET-01 deliberately contributes both existing trajectory seams.
119
+ */
120
+ export function exportTraceRun(sliceIds: readonly SubjectiveSliceId[]): OracleTraceRun {
121
+ const attestations = createOracleTraceAttestationRegistry()
122
+ const realTraces: OracleRealTrace[] = []
123
+ for (const sliceId of sliceIds) {
124
+ if (sliceId === SHEET_01_SLICE_ID) {
125
+ realTraces.push(
126
+ exportTraceInto(sliceId, attestations, 'native-core'),
127
+ exportTraceInto(sliceId, attestations, 'motion-dom'),
128
+ )
129
+ } else {
130
+ realTraces.push(exportTraceInto(sliceId, attestations, 'native-core'))
131
+ }
132
+ }
133
+ return { realTraces, attestations }
134
+ }