@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.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/package.json +33 -0
- package/src/adapter.ts +42 -0
- package/src/adapters/motion-dom.ts +96 -0
- package/src/adapters/native.ts +95 -0
- package/src/authoring.ts +78 -0
- package/src/comparator.ts +129 -0
- package/src/config.ts +22 -0
- package/src/declarations.ts +21 -0
- package/src/index.ts +144 -0
- package/src/oracle/attestation.ts +100 -0
- package/src/oracle/constants.ts +24 -0
- package/src/oracle/controls.ts +202 -0
- package/src/oracle/errors.ts +12 -0
- package/src/oracle/exportTrace.ts +134 -0
- package/src/oracle/index.ts +133 -0
- package/src/oracle/judge.ts +1374 -0
- package/src/oracle/presenter.ts +372 -0
- package/src/oracle/runRecord.ts +307 -0
- package/src/oracle/scenarios.ts +115 -0
- package/src/oracle/scripts/gesture.ts +218 -0
- package/src/oracle/serialize.ts +91 -0
- package/src/oracle/sweep.ts +155 -0
- package/src/oracle/types.ts +76 -0
- package/src/oracle/velocity.ts +44 -0
- package/src/parity.ts +136 -0
- package/src/runner.ts +168 -0
- package/src/scenario.ts +179 -0
- package/src/scenarios/appstore-choreography.ts +105 -0
- package/src/scenarios/component.ts +516 -0
- package/src/scenarios/driver.ts +322 -0
- package/src/scenarios/gesture.ts +363 -0
- package/src/scenarios/layout-identity.ts +264 -0
- package/src/scenarios/layout.ts +258 -0
- package/src/scenarios/presence.ts +302 -0
- package/src/scenarios/spring.ts +180 -0
- package/src/scenarios/value-types.ts +107 -0
- 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
|
+
}
|