@variance-authority/scenario 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ import { canonicalize, digestString, } from '@variance-authority/core';
2
+ import { checkedDefinition, checkedExecution, checkedRun } from './contract.js';
3
+ export function defineScenario(id, acts) {
4
+ requireName(id, 'scenario id');
5
+ for (const act of acts)
6
+ requireName(act.key, 'act key');
7
+ return checkedDefinition({
8
+ scenarioVersion: 1,
9
+ id,
10
+ acts: acts.map((act) => ({ ...act })),
11
+ });
12
+ }
13
+ export function unobserved(...diagnostics) {
14
+ if (diagnostics.length === 0) {
15
+ throw new Error('an unobserved scenario outcome requires at least one diagnostic');
16
+ }
17
+ return { kind: 'unobserved', diagnostics };
18
+ }
19
+ /** Content address of the complete canonical semantic object, not its render identity. */
20
+ export function semanticSnapshotDigest(snapshot) {
21
+ return digestString(canonicalize(snapshot));
22
+ }
23
+ export function startScenario(definition, options, arrange) {
24
+ requireName(options.id, 'execution id');
25
+ // Named here rather than left to the first read of them. The first read is a
26
+ // property access *on* the value that is missing, so an omitted
27
+ // `precondition` answered `Cannot read properties of undefined (reading
28
+ // 'id')` from inside this module while its neighbour on the same options
29
+ // object answered `execution id must not be empty`. Three options are
30
+ // required and the caller supplies all three; they say so the same way.
31
+ requirePrecondition(options.precondition);
32
+ requireName(options.profile, 'execution profile');
33
+ const snapshots = new Map();
34
+ const outcome = outcomeOf(arrange, options, snapshots);
35
+ const execution = checkedExecution({
36
+ executionVersion: 1,
37
+ id: options.id,
38
+ definition: definition.id,
39
+ precondition: options.precondition,
40
+ ...(options.preconditionLink !== undefined
41
+ ? { preconditionLink: options.preconditionLink }
42
+ : {}),
43
+ profile: options.profile,
44
+ frames: [{ at: 0, outcome }],
45
+ ...(outcome.kind === 'unobserved' ? { termination: outcome.diagnostics } : {}),
46
+ });
47
+ const run = checkedRun({
48
+ definition,
49
+ execution,
50
+ snapshots,
51
+ });
52
+ assertScenarioRun(run);
53
+ return run;
54
+ }
55
+ export function recordAct(run, key, observation) {
56
+ assertScenarioRun(run);
57
+ if (run.execution.termination !== undefined) {
58
+ throw new Error('refusing to record an act after the scenario became unobserved');
59
+ }
60
+ const actIndex = run.execution.frames.length - 1;
61
+ const expected = run.definition.acts[actIndex];
62
+ if (expected === undefined) {
63
+ throw new Error(`refusing to record \`${key}\`: scenario \`${run.definition.id}\` has ended`);
64
+ }
65
+ if (expected.key !== key) {
66
+ throw new Error(`refusing to record \`${key}\` at act ${actIndex + 1}: scenario \`${run.definition.id}\` expects \`${expected.key}\``);
67
+ }
68
+ const snapshots = new Map(run.snapshots);
69
+ const outcome = outcomeOf(observation, run.execution, snapshots);
70
+ const occurrence = run.execution.frames.filter((frame) => frame.act?.key === key).length + 1;
71
+ const frame = {
72
+ at: run.execution.frames.length,
73
+ act: { key, occurrence },
74
+ outcome,
75
+ };
76
+ const execution = checkedExecution({
77
+ ...run.execution,
78
+ frames: [...run.execution.frames, frame],
79
+ ...(outcome.kind === 'unobserved' ? { termination: outcome.diagnostics } : {}),
80
+ });
81
+ const recorded = checkedRun({
82
+ definition: run.definition,
83
+ execution,
84
+ snapshots,
85
+ });
86
+ assertScenarioRun(recorded);
87
+ return recorded;
88
+ }
89
+ /** Runtime guard for values crossing an archive or untyped host boundary. */
90
+ export function assertScenarioRun(run) {
91
+ if (run.definition.scenarioVersion !== 1 || run.execution.executionVersion !== 1) {
92
+ throw new Error('refusing scenario value with an unsupported version');
93
+ }
94
+ requireName(run.definition.id, 'scenario id');
95
+ requireName(run.execution.id, 'execution id');
96
+ if (run.execution.definition !== run.definition.id) {
97
+ throw new Error('refusing scenario execution whose definition identity does not match');
98
+ }
99
+ if (run.execution.frames.length === 0) {
100
+ throw new Error('refusing scenario execution without an Arrange frame');
101
+ }
102
+ const occurrences = new Map();
103
+ for (const [index, frame] of run.execution.frames.entries()) {
104
+ if (frame.at !== index) {
105
+ throw new Error(`refusing scenario frame ${index} whose ordinal is ${frame.at}`);
106
+ }
107
+ if (index === 0 && frame.act !== undefined) {
108
+ throw new Error('refusing an Arrange frame carrying an Act');
109
+ }
110
+ if (index > 0) {
111
+ const expected = run.definition.acts[index - 1];
112
+ if (frame.act === undefined || expected === undefined || frame.act.key !== expected.key) {
113
+ throw new Error(`refusing scenario frame ${index} without its authored Act`);
114
+ }
115
+ const occurrence = (occurrences.get(frame.act.key) ?? 0) + 1;
116
+ occurrences.set(frame.act.key, occurrence);
117
+ if (frame.act.occurrence !== occurrence) {
118
+ throw new Error(`refusing scenario frame ${index} with a shifted Act occurrence`);
119
+ }
120
+ }
121
+ if (frame.outcome.kind === 'unobserved') {
122
+ if (frame.outcome.diagnostics.length === 0) {
123
+ throw new Error('refusing an unobserved scenario outcome without diagnostics');
124
+ }
125
+ if (index !== run.execution.frames.length - 1) {
126
+ throw new Error('refusing scenario frames after an unobserved outcome');
127
+ }
128
+ }
129
+ else {
130
+ const snapshot = run.snapshots.get(frame.outcome.snapshot);
131
+ if (snapshot === undefined) {
132
+ throw new Error(`refusing observed frame without snapshot ${frame.outcome.snapshot}`);
133
+ }
134
+ if (semanticSnapshotDigest(snapshot) !== frame.outcome.snapshot ||
135
+ snapshot.renderHash !== frame.outcome.state) {
136
+ throw new Error('refusing observed frame whose snapshot identity does not match');
137
+ }
138
+ if (snapshot.subject.id !== run.execution.precondition.id ||
139
+ snapshot.profile.id !== run.execution.profile) {
140
+ throw new Error('refusing observed frame outside its precondition or profile');
141
+ }
142
+ }
143
+ }
144
+ const terminal = run.execution.frames.at(-1).outcome;
145
+ if ((terminal.kind === 'unobserved') !== (run.execution.termination !== undefined)) {
146
+ throw new Error('refusing scenario execution whose termination disagrees with its last frame');
147
+ }
148
+ }
149
+ function outcomeOf(observation, basis, snapshots) {
150
+ if (isUnobserved(observation))
151
+ return observation;
152
+ if (observation.subject.id !== basis.precondition.id) {
153
+ throw new Error(`refusing snapshot for \`${observation.subject.id}\` in scenario for \`${basis.precondition.id}\``);
154
+ }
155
+ if (observation.profile.id !== basis.profile) {
156
+ throw new Error(`refusing ${observation.profile.id} snapshot in ${basis.profile} scenario execution`);
157
+ }
158
+ const snapshot = semanticSnapshotDigest(observation);
159
+ snapshots.set(snapshot, observation);
160
+ return { kind: 'observed', snapshot, state: observation.renderHash };
161
+ }
162
+ function isUnobserved(observation) {
163
+ return 'kind' in observation && observation.kind === 'unobserved';
164
+ }
165
+ function requireName(value, what) {
166
+ // Absent and blank are different mistakes and the caller fixes them
167
+ // differently, so they are not folded into one sentence. Absent is checked at
168
+ // all because the type saying `string` binds the compiler, not a host that
169
+ // built these options out of a config file.
170
+ if (typeof value !== 'string')
171
+ throw new Error(`${what} is required`);
172
+ if (value.trim() === '')
173
+ throw new Error(`${what} must not be empty`);
174
+ }
175
+ function requirePrecondition(value) {
176
+ if (value === null || typeof value !== 'object') {
177
+ throw new Error('execution precondition is required');
178
+ }
179
+ requireName(value.id, 'execution precondition id');
180
+ }
181
+ //# sourceMappingURL=execution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution.js","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n canonicalize,\n digestString,\n type CanonicalValue,\n type Diagnostic,\n type Digest,\n type SemanticSnapshot,\n} from '@variance-authority/core';\nimport type {\n PreconditionLink,\n ScenarioAct,\n ScenarioDefinition,\n ScenarioObservation,\n ScenarioOutcome,\n ScenarioRun,\n UnobservedScenarioOutcome,\n} from './contract.js';\nimport { checkedDefinition, checkedExecution, checkedRun } from './contract.js';\n\nexport interface StartScenarioOptions {\n readonly id: string;\n readonly precondition: SemanticSnapshot['subject'];\n readonly profile: SemanticSnapshot['profile']['id'];\n readonly preconditionLink?: PreconditionLink;\n}\n\nexport function defineScenario(id: string, acts: readonly ScenarioAct[]): ScenarioDefinition {\n requireName(id, 'scenario id');\n for (const act of acts) requireName(act.key, 'act key');\n return checkedDefinition({\n scenarioVersion: 1,\n id,\n acts: acts.map((act) => ({ ...act })),\n });\n}\n\nexport function unobserved(\n ...diagnostics: readonly Diagnostic[]\n): UnobservedScenarioOutcome {\n if (diagnostics.length === 0) {\n throw new Error('an unobserved scenario outcome requires at least one diagnostic');\n }\n return { kind: 'unobserved', diagnostics };\n}\n\n/** Content address of the complete canonical semantic object, not its render identity. */\nexport function semanticSnapshotDigest(snapshot: SemanticSnapshot): Digest {\n return digestString(canonicalize(snapshot as unknown as CanonicalValue));\n}\n\nexport function startScenario(\n definition: ScenarioDefinition,\n options: StartScenarioOptions,\n arrange: ScenarioObservation,\n): ScenarioRun {\n requireName(options.id, 'execution id');\n // Named here rather than left to the first read of them. The first read is a\n // property access *on* the value that is missing, so an omitted\n // `precondition` answered `Cannot read properties of undefined (reading\n // 'id')` from inside this module while its neighbour on the same options\n // object answered `execution id must not be empty`. Three options are\n // required and the caller supplies all three; they say so the same way.\n requirePrecondition(options.precondition);\n requireName(options.profile, 'execution profile');\n const snapshots = new Map<string, SemanticSnapshot>();\n const outcome = outcomeOf(arrange, options, snapshots);\n\n const execution = checkedExecution({\n executionVersion: 1,\n id: options.id,\n definition: definition.id,\n precondition: options.precondition,\n ...(options.preconditionLink !== undefined\n ? { preconditionLink: options.preconditionLink }\n : {}),\n profile: options.profile,\n frames: [{ at: 0, outcome }],\n ...(outcome.kind === 'unobserved' ? { termination: outcome.diagnostics } : {}),\n });\n const run = checkedRun({\n definition,\n execution,\n snapshots,\n });\n assertScenarioRun(run);\n return run;\n}\n\nexport function recordAct(\n run: ScenarioRun,\n key: string,\n observation: ScenarioObservation,\n): ScenarioRun {\n assertScenarioRun(run);\n if (run.execution.termination !== undefined) {\n throw new Error('refusing to record an act after the scenario became unobserved');\n }\n\n const actIndex = run.execution.frames.length - 1;\n const expected = run.definition.acts[actIndex];\n if (expected === undefined) {\n throw new Error(`refusing to record \\`${key}\\`: scenario \\`${run.definition.id}\\` has ended`);\n }\n if (expected.key !== key) {\n throw new Error(\n `refusing to record \\`${key}\\` at act ${actIndex + 1}: scenario \\`${run.definition.id}\\` expects \\`${expected.key}\\``,\n );\n }\n\n const snapshots = new Map(run.snapshots);\n const outcome = outcomeOf(observation, run.execution, snapshots);\n const occurrence =\n run.execution.frames.filter((frame) => frame.act?.key === key).length + 1;\n const frame = {\n at: run.execution.frames.length,\n act: { key, occurrence },\n outcome,\n } as const;\n\n const execution = checkedExecution({\n ...run.execution,\n frames: [...run.execution.frames, frame],\n ...(outcome.kind === 'unobserved' ? { termination: outcome.diagnostics } : {}),\n });\n const recorded = checkedRun({\n definition: run.definition,\n execution,\n snapshots,\n });\n assertScenarioRun(recorded);\n return recorded;\n}\n\n/** Runtime guard for values crossing an archive or untyped host boundary. */\nexport function assertScenarioRun(run: ScenarioRun): void {\n if (run.definition.scenarioVersion !== 1 || run.execution.executionVersion !== 1) {\n throw new Error('refusing scenario value with an unsupported version');\n }\n requireName(run.definition.id, 'scenario id');\n requireName(run.execution.id, 'execution id');\n if (run.execution.definition !== run.definition.id) {\n throw new Error('refusing scenario execution whose definition identity does not match');\n }\n if (run.execution.frames.length === 0) {\n throw new Error('refusing scenario execution without an Arrange frame');\n }\n\n const occurrences = new Map<string, number>();\n for (const [index, frame] of run.execution.frames.entries()) {\n if (frame.at !== index) {\n throw new Error(`refusing scenario frame ${index} whose ordinal is ${frame.at}`);\n }\n if (index === 0 && frame.act !== undefined) {\n throw new Error('refusing an Arrange frame carrying an Act');\n }\n if (index > 0) {\n const expected = run.definition.acts[index - 1];\n if (frame.act === undefined || expected === undefined || frame.act.key !== expected.key) {\n throw new Error(`refusing scenario frame ${index} without its authored Act`);\n }\n const occurrence = (occurrences.get(frame.act.key) ?? 0) + 1;\n occurrences.set(frame.act.key, occurrence);\n if (frame.act.occurrence !== occurrence) {\n throw new Error(`refusing scenario frame ${index} with a shifted Act occurrence`);\n }\n }\n\n if (frame.outcome.kind === 'unobserved') {\n if (frame.outcome.diagnostics.length === 0) {\n throw new Error('refusing an unobserved scenario outcome without diagnostics');\n }\n if (index !== run.execution.frames.length - 1) {\n throw new Error('refusing scenario frames after an unobserved outcome');\n }\n } else {\n const snapshot = run.snapshots.get(frame.outcome.snapshot);\n if (snapshot === undefined) {\n throw new Error(`refusing observed frame without snapshot ${frame.outcome.snapshot}`);\n }\n if (\n semanticSnapshotDigest(snapshot) !== frame.outcome.snapshot ||\n snapshot.renderHash !== frame.outcome.state\n ) {\n throw new Error('refusing observed frame whose snapshot identity does not match');\n }\n if (\n snapshot.subject.id !== run.execution.precondition.id ||\n snapshot.profile.id !== run.execution.profile\n ) {\n throw new Error('refusing observed frame outside its precondition or profile');\n }\n }\n }\n\n const terminal = run.execution.frames.at(-1)!.outcome;\n if ((terminal.kind === 'unobserved') !== (run.execution.termination !== undefined)) {\n throw new Error('refusing scenario execution whose termination disagrees with its last frame');\n }\n}\n\nfunction outcomeOf(\n observation: ScenarioObservation,\n basis: Pick<StartScenarioOptions, 'precondition' | 'profile'>,\n snapshots: Map<string, SemanticSnapshot>,\n): ScenarioOutcome {\n if (isUnobserved(observation)) return observation;\n if (observation.subject.id !== basis.precondition.id) {\n throw new Error(\n `refusing snapshot for \\`${observation.subject.id}\\` in scenario for \\`${basis.precondition.id}\\``,\n );\n }\n if (observation.profile.id !== basis.profile) {\n throw new Error(\n `refusing ${observation.profile.id} snapshot in ${basis.profile} scenario execution`,\n );\n }\n const snapshot = semanticSnapshotDigest(observation);\n snapshots.set(snapshot, observation);\n return { kind: 'observed', snapshot, state: observation.renderHash };\n}\n\nfunction isUnobserved(\n observation: ScenarioObservation,\n): observation is UnobservedScenarioOutcome {\n return 'kind' in observation && observation.kind === 'unobserved';\n}\n\nfunction requireName(value: string, what: string): void {\n // Absent and blank are different mistakes and the caller fixes them\n // differently, so they are not folded into one sentence. Absent is checked at\n // all because the type saying `string` binds the compiler, not a host that\n // built these options out of a config file.\n if (typeof value !== 'string') throw new Error(`${what} is required`);\n if (value.trim() === '') throw new Error(`${what} must not be empty`);\n}\n\nfunction requirePrecondition(value: SemanticSnapshot['subject']): void {\n if (value === null || typeof value !== 'object') {\n throw new Error('execution precondition is required');\n }\n requireName(value.id, 'execution precondition id');\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Runtime scenarios: named Arrange states, authored Act transitions, and variance assessment.
3
+ *
4
+ * The package is inquiry only. Its values do not write baselines, history, approvals, or exit
5
+ * codes. Durable text retention is an explicit second entrypoint at `scenario/archive`.
6
+ */
7
+ export type { PreconditionAxisStep, PreconditionLink, ScenarioAct, ScenarioActRef, ScenarioAssessment, ScenarioBlindSide, ScenarioComparison, ScenarioDefinition, ScenarioDivergence, ScenarioEffectDivergence, ScenarioExecution, ScenarioFrame, ScenarioObservation, ScenarioOutcome, ScenarioParting, ScenarioRun, ScenarioTransitionAssessment, ScenarioUnmatchedAct, ScenarioVariance, UnobservedScenarioOutcome, } from './contract.js';
8
+ export { defineScenario, recordAct, semanticSnapshotDigest, startScenario, unobserved, } from './execution.js';
9
+ export type { StartScenarioOptions } from './execution.js';
10
+ export { assessScenarios } from './assessment.js';
11
+ export { foldScenarios } from './machine.js';
12
+ export type { ScenarioMachine, ScenarioMachineDivergence, ScenarioMachineNode, ScenarioMachineTransition, ScenarioUnknownTransition, } from './machine.js';
13
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Runtime scenarios: named Arrange states, authored Act transitions, and variance assessment.
3
+ *
4
+ * The package is inquiry only. Its values do not write baselines, history, approvals, or exit
5
+ * codes. Durable text retention is an explicit second entrypoint at `scenario/archive`.
6
+ */
7
+ export { defineScenario, recordAct, semanticSnapshotDigest, startScenario, unobserved, } from './execution.js';
8
+ export { assessScenarios } from './assessment.js';
9
+ export { foldScenarios } from './machine.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwBH,OAAO,EACL,cAAc,EACd,SAAS,EACT,sBAAsB,EACtB,aAAa,EACb,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Runtime scenarios: named Arrange states, authored Act transitions, and variance assessment.\n *\n * The package is inquiry only. Its values do not write baselines, history, approvals, or exit\n * codes. Durable text retention is an explicit second entrypoint at `scenario/archive`.\n */\n\nexport type {\n PreconditionAxisStep,\n PreconditionLink,\n ScenarioAct,\n ScenarioActRef,\n ScenarioAssessment,\n ScenarioBlindSide,\n ScenarioComparison,\n ScenarioDefinition,\n ScenarioDivergence,\n ScenarioEffectDivergence,\n ScenarioExecution,\n ScenarioFrame,\n ScenarioObservation,\n ScenarioOutcome,\n ScenarioParting,\n ScenarioRun,\n ScenarioTransitionAssessment,\n ScenarioUnmatchedAct,\n ScenarioVariance,\n UnobservedScenarioOutcome,\n} from './contract.js';\nexport {\n defineScenario,\n recordAct,\n semanticSnapshotDigest,\n startScenario,\n unobserved,\n} from './execution.js';\nexport type { StartScenarioOptions } from './execution.js';\nexport { assessScenarios } from './assessment.js';\nexport { foldScenarios } from './machine.js';\nexport type {\n ScenarioMachine,\n ScenarioMachineDivergence,\n ScenarioMachineNode,\n ScenarioMachineTransition,\n ScenarioUnknownTransition,\n} from './machine.js';\n"]}
@@ -0,0 +1,33 @@
1
+ import type { Digest } from '@variance-authority/core';
2
+ import type { ScenarioActRef, ScenarioRun } from './contract.js';
3
+ export interface ScenarioMachineNode {
4
+ readonly state: Digest;
5
+ readonly snapshots: readonly Digest[];
6
+ readonly executions: readonly string[];
7
+ }
8
+ export interface ScenarioMachineTransition {
9
+ readonly from: Digest;
10
+ readonly act: string;
11
+ readonly to: Digest;
12
+ readonly executions: readonly string[];
13
+ }
14
+ export interface ScenarioUnknownTransition {
15
+ readonly from: Digest;
16
+ readonly act: ScenarioActRef;
17
+ readonly execution: string;
18
+ readonly because: string;
19
+ }
20
+ export interface ScenarioMachineDivergence {
21
+ readonly from: Digest;
22
+ readonly act: string;
23
+ readonly destinations: readonly Digest[];
24
+ }
25
+ export interface ScenarioMachine {
26
+ readonly nodes: readonly ScenarioMachineNode[];
27
+ readonly transitions: readonly ScenarioMachineTransition[];
28
+ readonly unknown: readonly ScenarioUnknownTransition[];
29
+ readonly divergences: readonly ScenarioMachineDivergence[];
30
+ }
31
+ /** Fold witnessed execution paths into a partial state machine. No absent edge is inferred. */
32
+ export declare function foldScenarios(runs: readonly ScenarioRun[]): ScenarioMachine;
33
+ //# sourceMappingURL=machine.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { assertScenarioRun } from './execution.js';
2
+ /** Fold witnessed execution paths into a partial state machine. No absent edge is inferred. */
3
+ export function foldScenarios(runs) {
4
+ const nodes = new Map();
5
+ const transitions = new Map();
6
+ const unknown = [];
7
+ for (const run of runs) {
8
+ assertScenarioRun(run);
9
+ for (const frame of run.execution.frames) {
10
+ if (frame.outcome.kind !== 'observed')
11
+ continue;
12
+ const node = nodes.get(frame.outcome.state) ?? {
13
+ snapshots: new Set(),
14
+ executions: new Set(),
15
+ };
16
+ node.snapshots.add(frame.outcome.snapshot);
17
+ node.executions.add(run.execution.id);
18
+ nodes.set(frame.outcome.state, node);
19
+ }
20
+ for (let index = 1; index < run.execution.frames.length; index += 1) {
21
+ const before = run.execution.frames[index - 1];
22
+ const after = run.execution.frames[index];
23
+ if (before.outcome.kind !== 'observed' || after.act === undefined)
24
+ continue;
25
+ if (after.outcome.kind === 'unobserved') {
26
+ unknown.push({
27
+ from: before.outcome.state,
28
+ act: after.act,
29
+ execution: run.execution.id,
30
+ because: after.outcome.diagnostics.map((entry) => entry.message).join('; '),
31
+ });
32
+ continue;
33
+ }
34
+ const key = `${before.outcome.state}\u0000${after.act.key}\u0000${after.outcome.state}`;
35
+ const edge = transitions.get(key) ?? {
36
+ from: before.outcome.state,
37
+ act: after.act.key,
38
+ to: after.outcome.state,
39
+ executions: new Set(),
40
+ };
41
+ edge.executions.add(run.execution.id);
42
+ transitions.set(key, edge);
43
+ }
44
+ }
45
+ const edges = [...transitions.values()]
46
+ .map((edge) => ({ ...edge, executions: sorted(edge.executions) }))
47
+ .sort((a, b) => compare(`${a.from}\u0000${a.act}\u0000${a.to}`, `${b.from}\u0000${b.act}\u0000${b.to}`));
48
+ const destinations = new Map();
49
+ for (const edge of edges) {
50
+ const key = `${edge.from}\u0000${edge.act}`;
51
+ const found = destinations.get(key) ?? new Set();
52
+ found.add(edge.to);
53
+ destinations.set(key, found);
54
+ }
55
+ return {
56
+ nodes: [...nodes.entries()]
57
+ .map(([state, node]) => ({
58
+ state,
59
+ snapshots: sorted(node.snapshots),
60
+ executions: sorted(node.executions),
61
+ }))
62
+ .sort((a, b) => compare(a.state, b.state)),
63
+ transitions: edges,
64
+ unknown: [...unknown].sort((a, b) => compare(`${a.from}\u0000${a.act.key}\u0000${a.execution}`, `${b.from}\u0000${b.act.key}\u0000${b.execution}`)),
65
+ divergences: [...destinations.entries()]
66
+ .filter(([, values]) => values.size > 1)
67
+ .map(([key, values]) => {
68
+ const split = key.indexOf('\u0000');
69
+ return {
70
+ from: key.slice(0, split),
71
+ act: key.slice(split + 1),
72
+ destinations: sorted(values),
73
+ };
74
+ })
75
+ .sort((a, b) => compare(`${a.from}\u0000${a.act}`, `${b.from}\u0000${b.act}`)),
76
+ };
77
+ }
78
+ function sorted(values) {
79
+ return [...values].sort(compare);
80
+ }
81
+ function compare(left, right) {
82
+ return left < right ? -1 : left > right ? 1 : 0;
83
+ }
84
+ //# sourceMappingURL=machine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine.js","sourceRoot":"","sources":["../src/machine.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '@variance-authority/core';\nimport type { ScenarioActRef, ScenarioRun } from './contract.js';\nimport { assertScenarioRun } from './execution.js';\n\nexport interface ScenarioMachineNode {\n readonly state: Digest;\n readonly snapshots: readonly Digest[];\n readonly executions: readonly string[];\n}\n\nexport interface ScenarioMachineTransition {\n readonly from: Digest;\n readonly act: string;\n readonly to: Digest;\n readonly executions: readonly string[];\n}\n\nexport interface ScenarioUnknownTransition {\n readonly from: Digest;\n readonly act: ScenarioActRef;\n readonly execution: string;\n readonly because: string;\n}\n\nexport interface ScenarioMachineDivergence {\n readonly from: Digest;\n readonly act: string;\n readonly destinations: readonly Digest[];\n}\n\nexport interface ScenarioMachine {\n readonly nodes: readonly ScenarioMachineNode[];\n readonly transitions: readonly ScenarioMachineTransition[];\n readonly unknown: readonly ScenarioUnknownTransition[];\n readonly divergences: readonly ScenarioMachineDivergence[];\n}\n\n/** Fold witnessed execution paths into a partial state machine. No absent edge is inferred. */\nexport function foldScenarios(runs: readonly ScenarioRun[]): ScenarioMachine {\n const nodes = new Map<Digest, { snapshots: Set<Digest>; executions: Set<string> }>();\n const transitions = new Map<\n string,\n { from: Digest; act: string; to: Digest; executions: Set<string> }\n >();\n const unknown: ScenarioUnknownTransition[] = [];\n\n for (const run of runs) {\n assertScenarioRun(run);\n for (const frame of run.execution.frames) {\n if (frame.outcome.kind !== 'observed') continue;\n const node = nodes.get(frame.outcome.state) ?? {\n snapshots: new Set<Digest>(),\n executions: new Set<string>(),\n };\n node.snapshots.add(frame.outcome.snapshot);\n node.executions.add(run.execution.id);\n nodes.set(frame.outcome.state, node);\n }\n\n for (let index = 1; index < run.execution.frames.length; index += 1) {\n const before = run.execution.frames[index - 1]!;\n const after = run.execution.frames[index]!;\n if (before.outcome.kind !== 'observed' || after.act === undefined) continue;\n if (after.outcome.kind === 'unobserved') {\n unknown.push({\n from: before.outcome.state,\n act: after.act,\n execution: run.execution.id,\n because: after.outcome.diagnostics.map((entry) => entry.message).join('; '),\n });\n continue;\n }\n\n const key = `${before.outcome.state}\\u0000${after.act.key}\\u0000${after.outcome.state}`;\n const edge = transitions.get(key) ?? {\n from: before.outcome.state,\n act: after.act.key,\n to: after.outcome.state,\n executions: new Set<string>(),\n };\n edge.executions.add(run.execution.id);\n transitions.set(key, edge);\n }\n }\n\n const edges = [...transitions.values()]\n .map((edge) => ({ ...edge, executions: sorted(edge.executions) }))\n .sort((a, b) => compare(`${a.from}\\u0000${a.act}\\u0000${a.to}`, `${b.from}\\u0000${b.act}\\u0000${b.to}`));\n const destinations = new Map<string, Set<Digest>>();\n for (const edge of edges) {\n const key = `${edge.from}\\u0000${edge.act}`;\n const found = destinations.get(key) ?? new Set<Digest>();\n found.add(edge.to);\n destinations.set(key, found);\n }\n\n return {\n nodes: [...nodes.entries()]\n .map(([state, node]) => ({\n state,\n snapshots: sorted(node.snapshots),\n executions: sorted(node.executions),\n }))\n .sort((a, b) => compare(a.state, b.state)),\n transitions: edges,\n unknown: [...unknown].sort((a, b) =>\n compare(`${a.from}\\u0000${a.act.key}\\u0000${a.execution}`, `${b.from}\\u0000${b.act.key}\\u0000${b.execution}`),\n ),\n divergences: [...destinations.entries()]\n .filter(([, values]) => values.size > 1)\n .map(([key, values]) => {\n const split = key.indexOf('\\u0000');\n return {\n from: key.slice(0, split),\n act: key.slice(split + 1),\n destinations: sorted(values),\n };\n })\n .sort((a, b) => compare(`${a.from}\\u0000${a.act}`, `${b.from}\\u0000${b.act}`)),\n };\n}\n\nfunction sorted(values: Iterable<string>): string[] {\n return [...values].sort(compare);\n}\n\nfunction compare(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
package/mark.svg ADDED
@@ -0,0 +1,30 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 320" role="img" aria-labelledby="title desc">
2
+ <title id="title">Variance Authority mark</title>
3
+ <desc id="desc">Folded ribbon VA mark in charcoal and orange.</desc>
4
+
5
+ <defs>
6
+ <style>
7
+ .ribbon-dark {
8
+ fill: #24282A;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ .ribbon-dark {
13
+ fill: #F3F4F6;
14
+ }
15
+ }
16
+ </style>
17
+ </defs>
18
+
19
+ <!-- Left descending ribbon -->
20
+ <path class="ribbon-dark" d="M64 54H159L256 266H160Z"/>
21
+
22
+ <!-- Right descending ribbon -->
23
+ <path class="ribbon-dark" d="M331 28H419L494 266H397Z"/>
24
+
25
+ <!-- Chosen / variant ribbon -->
26
+ <path d="M256 266L202 152L283 28H376L301 165Z" fill="#FF4A19"/>
27
+
28
+ <!-- Fold shadow -->
29
+ <path d="M202 152L256 266L301 165L264 103Z" fill="#D83A13" opacity="0.72"/>
30
+ </svg>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@variance-authority/scenario",
3
+ "version": "0.1.0",
4
+ "description": "Record runtime scenarios as AAA state machines and assess variance across witnessed transitions.",
5
+ "keywords": [
6
+ "variance-authority",
7
+ "state-machine",
8
+ "scenario",
9
+ "aaa",
10
+ "runtime"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Machine Garden",
14
+ "homepage": "https://variance-authority.dev#packages",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Variance-Authority/variance-authority.git",
18
+ "directory": "packages/scenario"
19
+ },
20
+ "bugs": "https://github.com/Variance-Authority/variance-authority/issues",
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./archive": {
31
+ "types": "./dist/archive.d.ts",
32
+ "default": "./dist/archive.js"
33
+ }
34
+ },
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "!dist/**/*.d.ts.map",
40
+ "mark.svg"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsc --build"
44
+ },
45
+ "dependencies": {
46
+ "@variance-authority/core": "^0.1.0"
47
+ }
48
+ }