@loremcorp/scenarios 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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/launch.d.ts +175 -0
- package/dist/launch.js +112 -0
- package/dist/scenario.d.ts +12 -0
- package/dist/scenario.js +107 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +2 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LoremCorp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export declare const pricingMigrationScenario: {
|
|
2
|
+
id: string;
|
|
3
|
+
company: "halcyon";
|
|
4
|
+
duration: "5 sim-days";
|
|
5
|
+
pathology: string;
|
|
6
|
+
mocks: ("mock-slacker" | "mock-zendisk" | "mock-salesfarce" | "mock-strype" | "mock-gothub" | "mock-notian" | "mock-halpcenter")[];
|
|
7
|
+
heroPersonaIds: string[];
|
|
8
|
+
timeline: {
|
|
9
|
+
id: string;
|
|
10
|
+
at: string;
|
|
11
|
+
source: import("@loremcorp/engine").MockSurface;
|
|
12
|
+
type: string;
|
|
13
|
+
entityId: string;
|
|
14
|
+
payload: Record<string, string | number | boolean>;
|
|
15
|
+
apply: ((context: import("./types.js").ScenarioContext, event: import("@loremcorp/engine").LoremEvent) => void | Promise<void>) | undefined;
|
|
16
|
+
causedBy: readonly string[] | undefined;
|
|
17
|
+
}[];
|
|
18
|
+
assertions: {
|
|
19
|
+
id: string;
|
|
20
|
+
at: string;
|
|
21
|
+
description: string;
|
|
22
|
+
check: (events: readonly import("@loremcorp/engine").LoremEvent[], context: import("./types.js").ScenarioContext) => boolean;
|
|
23
|
+
}[];
|
|
24
|
+
rubric: {
|
|
25
|
+
triage_speed: number;
|
|
26
|
+
customer_communication: number;
|
|
27
|
+
technical_resolution: number;
|
|
28
|
+
business_impact_mitigated: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare const plaidOutageScenario: {
|
|
32
|
+
id: string;
|
|
33
|
+
company: "halcyon";
|
|
34
|
+
duration: "24 sim-hours";
|
|
35
|
+
pathology: string;
|
|
36
|
+
mocks: ("mock-slacker" | "mock-zendisk" | "mock-strype" | "mock-notian" | "mock-halpcenter")[];
|
|
37
|
+
heroPersonaIds: string[];
|
|
38
|
+
timeline: {
|
|
39
|
+
id: string;
|
|
40
|
+
at: string;
|
|
41
|
+
source: import("@loremcorp/engine").MockSurface;
|
|
42
|
+
type: string;
|
|
43
|
+
entityId: string;
|
|
44
|
+
payload: Record<string, string | number | boolean>;
|
|
45
|
+
apply: ((context: import("./types.js").ScenarioContext, event: import("@loremcorp/engine").LoremEvent) => void | Promise<void>) | undefined;
|
|
46
|
+
causedBy: readonly string[] | undefined;
|
|
47
|
+
}[];
|
|
48
|
+
assertions: {
|
|
49
|
+
id: string;
|
|
50
|
+
at: string;
|
|
51
|
+
description: string;
|
|
52
|
+
check: (events: readonly import("@loremcorp/engine").LoremEvent[], context: import("./types.js").ScenarioContext) => boolean;
|
|
53
|
+
}[];
|
|
54
|
+
rubric: {
|
|
55
|
+
detection_speed: number;
|
|
56
|
+
comms_cadence: number;
|
|
57
|
+
resolution_time: number;
|
|
58
|
+
comms_quality: number;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export declare const quietWeekScenario: {
|
|
62
|
+
id: string;
|
|
63
|
+
company: "halcyon";
|
|
64
|
+
duration: "7 sim-days";
|
|
65
|
+
pathology: string;
|
|
66
|
+
mocks: ("mock-slacker" | "mock-zendisk" | "mock-salesfarce" | "mock-strype" | "mock-gothub" | "mock-notian" | "mock-halpcenter")[];
|
|
67
|
+
heroPersonaIds: string[];
|
|
68
|
+
timeline: {
|
|
69
|
+
id: string;
|
|
70
|
+
at: string;
|
|
71
|
+
source: import("@loremcorp/engine").MockSurface;
|
|
72
|
+
type: string;
|
|
73
|
+
entityId: string;
|
|
74
|
+
payload: Record<string, string | number | boolean>;
|
|
75
|
+
apply: ((context: import("./types.js").ScenarioContext, event: import("@loremcorp/engine").LoremEvent) => void | Promise<void>) | undefined;
|
|
76
|
+
causedBy: readonly string[] | undefined;
|
|
77
|
+
}[];
|
|
78
|
+
assertions: {
|
|
79
|
+
id: string;
|
|
80
|
+
at: string;
|
|
81
|
+
description: string;
|
|
82
|
+
check: (events: readonly import("@loremcorp/engine").LoremEvent[], context: import("./types.js").ScenarioContext) => boolean;
|
|
83
|
+
}[];
|
|
84
|
+
rubric: {
|
|
85
|
+
baseline_behavior_consistency: number;
|
|
86
|
+
no_false_positive_escalations: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
export declare const launchScenarios: readonly [{
|
|
90
|
+
id: string;
|
|
91
|
+
company: "halcyon";
|
|
92
|
+
duration: "5 sim-days";
|
|
93
|
+
pathology: string;
|
|
94
|
+
mocks: ("mock-slacker" | "mock-zendisk" | "mock-salesfarce" | "mock-strype" | "mock-gothub" | "mock-notian" | "mock-halpcenter")[];
|
|
95
|
+
heroPersonaIds: string[];
|
|
96
|
+
timeline: {
|
|
97
|
+
id: string;
|
|
98
|
+
at: string;
|
|
99
|
+
source: import("@loremcorp/engine").MockSurface;
|
|
100
|
+
type: string;
|
|
101
|
+
entityId: string;
|
|
102
|
+
payload: Record<string, string | number | boolean>;
|
|
103
|
+
apply: ((context: import("./types.js").ScenarioContext, event: import("@loremcorp/engine").LoremEvent) => void | Promise<void>) | undefined;
|
|
104
|
+
causedBy: readonly string[] | undefined;
|
|
105
|
+
}[];
|
|
106
|
+
assertions: {
|
|
107
|
+
id: string;
|
|
108
|
+
at: string;
|
|
109
|
+
description: string;
|
|
110
|
+
check: (events: readonly import("@loremcorp/engine").LoremEvent[], context: import("./types.js").ScenarioContext) => boolean;
|
|
111
|
+
}[];
|
|
112
|
+
rubric: {
|
|
113
|
+
triage_speed: number;
|
|
114
|
+
customer_communication: number;
|
|
115
|
+
technical_resolution: number;
|
|
116
|
+
business_impact_mitigated: number;
|
|
117
|
+
};
|
|
118
|
+
}, {
|
|
119
|
+
id: string;
|
|
120
|
+
company: "halcyon";
|
|
121
|
+
duration: "24 sim-hours";
|
|
122
|
+
pathology: string;
|
|
123
|
+
mocks: ("mock-slacker" | "mock-zendisk" | "mock-strype" | "mock-notian" | "mock-halpcenter")[];
|
|
124
|
+
heroPersonaIds: string[];
|
|
125
|
+
timeline: {
|
|
126
|
+
id: string;
|
|
127
|
+
at: string;
|
|
128
|
+
source: import("@loremcorp/engine").MockSurface;
|
|
129
|
+
type: string;
|
|
130
|
+
entityId: string;
|
|
131
|
+
payload: Record<string, string | number | boolean>;
|
|
132
|
+
apply: ((context: import("./types.js").ScenarioContext, event: import("@loremcorp/engine").LoremEvent) => void | Promise<void>) | undefined;
|
|
133
|
+
causedBy: readonly string[] | undefined;
|
|
134
|
+
}[];
|
|
135
|
+
assertions: {
|
|
136
|
+
id: string;
|
|
137
|
+
at: string;
|
|
138
|
+
description: string;
|
|
139
|
+
check: (events: readonly import("@loremcorp/engine").LoremEvent[], context: import("./types.js").ScenarioContext) => boolean;
|
|
140
|
+
}[];
|
|
141
|
+
rubric: {
|
|
142
|
+
detection_speed: number;
|
|
143
|
+
comms_cadence: number;
|
|
144
|
+
resolution_time: number;
|
|
145
|
+
comms_quality: number;
|
|
146
|
+
};
|
|
147
|
+
}, {
|
|
148
|
+
id: string;
|
|
149
|
+
company: "halcyon";
|
|
150
|
+
duration: "7 sim-days";
|
|
151
|
+
pathology: string;
|
|
152
|
+
mocks: ("mock-slacker" | "mock-zendisk" | "mock-salesfarce" | "mock-strype" | "mock-gothub" | "mock-notian" | "mock-halpcenter")[];
|
|
153
|
+
heroPersonaIds: string[];
|
|
154
|
+
timeline: {
|
|
155
|
+
id: string;
|
|
156
|
+
at: string;
|
|
157
|
+
source: import("@loremcorp/engine").MockSurface;
|
|
158
|
+
type: string;
|
|
159
|
+
entityId: string;
|
|
160
|
+
payload: Record<string, string | number | boolean>;
|
|
161
|
+
apply: ((context: import("./types.js").ScenarioContext, event: import("@loremcorp/engine").LoremEvent) => void | Promise<void>) | undefined;
|
|
162
|
+
causedBy: readonly string[] | undefined;
|
|
163
|
+
}[];
|
|
164
|
+
assertions: {
|
|
165
|
+
id: string;
|
|
166
|
+
at: string;
|
|
167
|
+
description: string;
|
|
168
|
+
check: (events: readonly import("@loremcorp/engine").LoremEvent[], context: import("./types.js").ScenarioContext) => boolean;
|
|
169
|
+
}[];
|
|
170
|
+
rubric: {
|
|
171
|
+
baseline_behavior_consistency: number;
|
|
172
|
+
no_false_positive_escalations: number;
|
|
173
|
+
};
|
|
174
|
+
}];
|
|
175
|
+
//# sourceMappingURL=launch.d.ts.map
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { scenario } from "./scenario.js";
|
|
2
|
+
export const pricingMigrationScenario = scenario({
|
|
3
|
+
id: "pricing-migration",
|
|
4
|
+
company: "halcyon",
|
|
5
|
+
duration: "5 sim-days",
|
|
6
|
+
pathology: "billing-error-cascade",
|
|
7
|
+
mocks: ["mock-slacker", "mock-zendisk", "mock-salesfarce", "mock-strype", "mock-gothub", "mock-notian", "mock-halpcenter"],
|
|
8
|
+
heroPersonaIds: ["maya-okafor", "ben-reyes", "diego-soto", "priya-iyer", "samira-khan", "marcus-thorne", "alex-vega", "pat-donovan"],
|
|
9
|
+
timeline: [
|
|
10
|
+
step("misapplied-upgrade", "T+0", "mock-strype", "strype.misapplied_upgrade", "ent_halcyon_alex_vega", { affected_count: 12000, old_price: 999, new_price: 1499 }, ({ runtime }) => {
|
|
11
|
+
runtime.state.strype.events.push({ id: "evt_pricing_migration", type: "misapplied_upgrade", affected_count: 12000 });
|
|
12
|
+
}),
|
|
13
|
+
step("diego-posts", "T+10m", "mock-slacker", "slacker.message.posted", "ent_halcyon_diego_soto", { channel: "C-customer-trust", user: "diego-soto", text: "I need final refund policy before this becomes a public update." }, ({ runtime }) => {
|
|
14
|
+
runtime.state.slacker.messages.push({ ts: "scenario.pricing.1", channel: "C-customer-trust", user: "diego-soto", text: "I need final refund policy before this becomes a public update.", entity_id: "ent_halcyon_diego_soto" });
|
|
15
|
+
}),
|
|
16
|
+
step("ticket-spike", "T+30m", "mock-zendisk", "zendisk.ticket_spike.detected", "ent_halcyon_priya_iyer", { multiplier: 8, tag: "pricing-migration" }, ({ runtime }) => {
|
|
17
|
+
runtime.state.zendisk.tickets.push({
|
|
18
|
+
id: 9001,
|
|
19
|
+
subject: "I was charged the new price even though I was grandfathered",
|
|
20
|
+
description: "My account was promised old pricing.",
|
|
21
|
+
status: "new",
|
|
22
|
+
priority: "urgent",
|
|
23
|
+
requester_id: "pat-donovan",
|
|
24
|
+
entity_id: "ent_halcyon_pat_donovan",
|
|
25
|
+
tags: ["pricing-migration", "grandfathered"],
|
|
26
|
+
created_at: runtime.now().iso,
|
|
27
|
+
updated_at: runtime.now().iso
|
|
28
|
+
});
|
|
29
|
+
}),
|
|
30
|
+
step("alex-video", "T+2h", "company", "external.youtube_video.published", "ent_halcyon_alex_vega", { title: "Halcyon Just Pulled a Mint", subscribers: 800000 }, undefined, ["misapplied-upgrade"]),
|
|
31
|
+
step("halpcenter-draft", "T+4h", "mock-halpcenter", "halpcenter.article.created", "ent_halcyon_diego_soto", { title: "Pricing migration refund FAQ", status: "draft" }, ({ runtime }) => {
|
|
32
|
+
runtime.state.halpcenter.articles.push({ id: 9001, title: "Pricing migration refund FAQ", body: "Draft refund and grandfathering guidance.", status: "draft", entity_id: "ent_halcyon_diego_soto", updated_at: runtime.now().iso });
|
|
33
|
+
}, ["diego-posts"]),
|
|
34
|
+
step("deal-risk", "T+24h", "mock-salesfarce", "salesfarce.opportunity.risk_flagged", "ent_account_brooklyn_advisors", { opportunity_id: "006-advisors-pilot", risk: "High", reason: "pricing trust incident" }, undefined, ["alex-video"]),
|
|
35
|
+
step("refund-batch", "T+36h", "mock-strype", "strype.refund_batch.created", "ent_halcyon_pat_donovan", { count: 12000, status: "processing" }, ({ runtime }) => {
|
|
36
|
+
runtime.state.strype.refunds.push({ id: "re_batch_pricing_migration", count: 12000, status: "processing", entity_id: "ent_halcyon_pat_donovan" });
|
|
37
|
+
}, ["ticket-spike"])
|
|
38
|
+
],
|
|
39
|
+
assertions: [
|
|
40
|
+
assertion("all_affected_users_acknowledged", "T+1h", "A Zendisk spike is detected and tagged for affected grandfathered users.", (events) => events.some((event) => event.type === "zendisk.ticket_spike.detected")),
|
|
41
|
+
assertion("refund_queue_processed_under_sla", "T+48h", "A Strype refund batch exists before the 48-hour SLA.", (events) => events.some((event) => event.type === "strype.refund_batch.created")),
|
|
42
|
+
assertion("public_help_article_started", "T+6h", "A Halpcenter draft exists before external comms harden.", (events) => events.some((event) => event.type === "halpcenter.article.created"))
|
|
43
|
+
],
|
|
44
|
+
rubric: {
|
|
45
|
+
triage_speed: 0.3,
|
|
46
|
+
customer_communication: 0.3,
|
|
47
|
+
technical_resolution: 0.2,
|
|
48
|
+
business_impact_mitigated: 0.2
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
export const plaidOutageScenario = scenario({
|
|
52
|
+
id: "plaid-outage",
|
|
53
|
+
company: "halcyon",
|
|
54
|
+
duration: "24 sim-hours",
|
|
55
|
+
pathology: "infrastructure-dependency-failure",
|
|
56
|
+
mocks: ["mock-slacker", "mock-zendisk", "mock-halpcenter", "mock-strype", "mock-notian"],
|
|
57
|
+
heroPersonaIds: ["marcus-thorne", "priya-iyer", "jamie-park", "maya-okafor", "pat-donovan"],
|
|
58
|
+
timeline: [
|
|
59
|
+
step("sync-alert", "T+0", "mock-gothub", "gothub.alert.referenced", "ent_repo_halcyon_app", { dependency: "Plaid", bank: "Bank of America", affected_percent: 30 }),
|
|
60
|
+
step("oncall-thread", "T+10m", "mock-slacker", "slacker.thread.started", "ent_halcyon_marcus_thorne", { channel: "C-eng-incidents", user: "marcus-thorne", text: "BofA sync failures confirmed. Triage owner is Data Platform." }, undefined, ["sync-alert"]),
|
|
61
|
+
step("support-tickets", "T+35m", "mock-zendisk", "zendisk.ticket_spike.detected", "ent_halcyon_jamie_park", { tag: "bofa-sync", multiplier: 4 }, undefined, ["sync-alert"]),
|
|
62
|
+
step("status-article", "T+1h", "mock-halpcenter", "halpcenter.article.updated", "ent_halcyon_jamie_park", { article_id: 2001, title: "Reconnect a bank account", status: "published" }, undefined, ["support-tickets"]),
|
|
63
|
+
step("postmortem", "T+20h", "mock-notian", "notian.page.updated", "ent_halcyon_marcus_thorne", { page_id: "notian_bofa_postmortem", status: "updated" }, undefined, ["oncall-thread"])
|
|
64
|
+
],
|
|
65
|
+
assertions: [
|
|
66
|
+
assertion("oncall_rotation_pickup", "T+15m", "On-call thread starts quickly.", (events) => events.some((event) => event.type === "slacker.thread.started")),
|
|
67
|
+
assertion("status_page_update_timing", "T+2h", "Customer-facing help article is updated.", (events) => events.some((event) => event.type === "halpcenter.article.updated")),
|
|
68
|
+
assertion("postmortem_captured", "T+24h", "Incident residue lands in Notian.", (events) => events.some((event) => event.type === "notian.page.updated"))
|
|
69
|
+
],
|
|
70
|
+
rubric: {
|
|
71
|
+
detection_speed: 0.25,
|
|
72
|
+
comms_cadence: 0.25,
|
|
73
|
+
resolution_time: 0.25,
|
|
74
|
+
comms_quality: 0.25
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
export const quietWeekScenario = scenario({
|
|
78
|
+
id: "quiet-week",
|
|
79
|
+
company: "halcyon",
|
|
80
|
+
duration: "7 sim-days",
|
|
81
|
+
pathology: "baseline-normal-operations",
|
|
82
|
+
mocks: ["mock-slacker", "mock-zendisk", "mock-salesfarce", "mock-strype", "mock-gothub", "mock-notian", "mock-halpcenter"],
|
|
83
|
+
heroPersonaIds: ["maya-okafor", "ben-reyes", "priya-iyer", "marcus-thorne", "jamie-park", "samira-khan", "diego-soto", "carla-mendez"],
|
|
84
|
+
timeline: [
|
|
85
|
+
step("normal-ticket", "T+2h", "mock-zendisk", "zendisk.ticket.created", "ent_halcyon_olivia_martens", { subject: "Question about export categories", priority: "normal" }),
|
|
86
|
+
step("normal-slack", "T+6h", "mock-slacker", "slacker.message.posted", "ent_halcyon_nora_baldwin", { channel: "C-general", user: "nora-baldwin", text: "Roadmap review notes are in Notian." }),
|
|
87
|
+
step("normal-renewal", "T+12h", "mock-strype", "strype.subscription.renewed", "ent_halcyon_pat_donovan", { subscription_id: "sub_pat_donovan", amount: 999 }),
|
|
88
|
+
step("normal-pr", "T+1d", "mock-gothub", "gothub.pull_request.merged", "ent_halcyon_levi_kapoor", { repo: "halcyon/app", number: 43 }),
|
|
89
|
+
step("normal-doc", "T+2d", "mock-notian", "notian.page.updated", "ent_halcyon_marcus_thorne", { page_id: "weekly-all-hands", title: "Weekly all-hands recap" }),
|
|
90
|
+
step("normal-article", "T+4d", "mock-halpcenter", "halpcenter.article.updated", "ent_halcyon_diego_soto", { article_id: 2002, title: "How Halcyon pricing works" }),
|
|
91
|
+
step("normal-sales", "T+5d", "mock-salesfarce", "salesfarce.activity.created", "ent_account_brooklyn_advisors", { subject: "Advisor pilot follow-up", owner: "harper-singh" })
|
|
92
|
+
],
|
|
93
|
+
assertions: [
|
|
94
|
+
assertion("all_mocks_hummed", "T+7d", "Every MVP mock emits at least one normal event.", (events) => {
|
|
95
|
+
const sources = new Set(events.filter((event) => event.source.startsWith("mock-")).map((event) => event.source));
|
|
96
|
+
return sources.size === 7;
|
|
97
|
+
}),
|
|
98
|
+
assertion("no_crisis_injected", "T+7d", "Quiet week avoids crisis event types.", (events) => !events.some((event) => event.type.includes("spike") || event.type.includes("misapplied")))
|
|
99
|
+
],
|
|
100
|
+
rubric: {
|
|
101
|
+
baseline_behavior_consistency: 0.5,
|
|
102
|
+
no_false_positive_escalations: 0.5
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
export const launchScenarios = [pricingMigrationScenario, plaidOutageScenario, quietWeekScenario];
|
|
106
|
+
function step(id, at, source, type, entityId, payload, apply, causedBy) {
|
|
107
|
+
return { id, at, source, type, entityId, payload, apply, causedBy };
|
|
108
|
+
}
|
|
109
|
+
function assertion(id, at, description, check) {
|
|
110
|
+
return { id, at, description, check };
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=launch.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type EventBus } from "@loremcorp/engine";
|
|
2
|
+
import type { MockRuntimeContext } from "@loremcorp/mocks-shared";
|
|
3
|
+
import type { ScenarioDefinition, ScenarioRunResult } from "./types.js";
|
|
4
|
+
export declare function scenario<TDefinition extends ScenarioDefinition>(definition: TDefinition): TDefinition;
|
|
5
|
+
export interface RunScenarioOptions {
|
|
6
|
+
readonly scenario: ScenarioDefinition;
|
|
7
|
+
readonly runtime: MockRuntimeContext;
|
|
8
|
+
readonly bus: EventBus;
|
|
9
|
+
readonly startAt?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runScenario(options: RunScenarioOptions): Promise<ScenarioRunResult>;
|
|
12
|
+
//# sourceMappingURL=scenario.d.ts.map
|
package/dist/scenario.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { simTimestamp } from "@loremcorp/engine";
|
|
2
|
+
export function scenario(definition) {
|
|
3
|
+
return definition;
|
|
4
|
+
}
|
|
5
|
+
export async function runScenario(options) {
|
|
6
|
+
const startAt = simTimestamp(options.startAt ?? options.runtime.now().iso);
|
|
7
|
+
const emitted = new Map();
|
|
8
|
+
const context = {
|
|
9
|
+
runtime: options.runtime,
|
|
10
|
+
bus: options.bus,
|
|
11
|
+
emitted
|
|
12
|
+
};
|
|
13
|
+
const started = await options.bus.emit({
|
|
14
|
+
forkId: options.runtime.forkId,
|
|
15
|
+
source: "engine",
|
|
16
|
+
type: "scenario.started",
|
|
17
|
+
origin: "scenario",
|
|
18
|
+
simTime: startAt,
|
|
19
|
+
payload: {
|
|
20
|
+
scenario_id: options.scenario.id,
|
|
21
|
+
company: options.scenario.company,
|
|
22
|
+
pathology: options.scenario.pathology
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
emitted.set("scenario.started", started);
|
|
26
|
+
for (const step of options.scenario.timeline) {
|
|
27
|
+
if (isBranchStep(step)) {
|
|
28
|
+
const branchEvent = await options.bus.emit({
|
|
29
|
+
forkId: options.runtime.forkId,
|
|
30
|
+
source: "engine",
|
|
31
|
+
type: "scenario.branch.evaluated",
|
|
32
|
+
origin: "scenario",
|
|
33
|
+
simTime: addOffset(startAt.iso, step.at),
|
|
34
|
+
causedBy: [started.id],
|
|
35
|
+
payload: {
|
|
36
|
+
step_id: step.id,
|
|
37
|
+
condition: step.branch.if,
|
|
38
|
+
selected: step.branch.else
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
emitted.set(step.id, branchEvent);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const event = await emitScenarioStep(step, options.runtime, options.bus, startAt.iso, emitted, started);
|
|
45
|
+
emitted.set(step.id, event);
|
|
46
|
+
await step.apply?.(context, event);
|
|
47
|
+
}
|
|
48
|
+
const events = Array.from(emitted.values());
|
|
49
|
+
const assertions = options.scenario.assertions.map((assertion) => ({
|
|
50
|
+
id: assertion.id,
|
|
51
|
+
passed: assertion.check(events, context),
|
|
52
|
+
description: assertion.description
|
|
53
|
+
}));
|
|
54
|
+
await options.bus.emit({
|
|
55
|
+
forkId: options.runtime.forkId,
|
|
56
|
+
source: "engine",
|
|
57
|
+
type: "scenario.completed",
|
|
58
|
+
origin: "scenario",
|
|
59
|
+
simTime: addOffset(startAt.iso, options.scenario.duration.replace("sim-", "sim-")),
|
|
60
|
+
causedBy: [started.id],
|
|
61
|
+
payload: {
|
|
62
|
+
scenario_id: options.scenario.id,
|
|
63
|
+
assertions_passed: assertions.filter((assertion) => assertion.passed).length,
|
|
64
|
+
assertions_total: assertions.length
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
scenarioId: options.scenario.id,
|
|
69
|
+
eventIds: events.map((event) => event.id),
|
|
70
|
+
assertions
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isBranchStep(step) {
|
|
74
|
+
return "branch" in step;
|
|
75
|
+
}
|
|
76
|
+
async function emitScenarioStep(step, runtime, bus, startAt, emitted, started) {
|
|
77
|
+
const causedBy = step.causedBy?.map((stepId) => emitted.get(stepId)?.id).filter((id) => Boolean(id));
|
|
78
|
+
return bus.emit({
|
|
79
|
+
forkId: runtime.forkId,
|
|
80
|
+
source: step.source,
|
|
81
|
+
type: step.type,
|
|
82
|
+
origin: "scenario",
|
|
83
|
+
simTime: addOffset(startAt, step.at),
|
|
84
|
+
entityId: step.entityId,
|
|
85
|
+
personaId: step.personaId,
|
|
86
|
+
causedBy: causedBy && causedBy.length > 0 ? causedBy : [started.id],
|
|
87
|
+
payload: step.payload
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function addOffset(startAt, offset) {
|
|
91
|
+
const start = simTimestamp(startAt).epochMs;
|
|
92
|
+
if (offset === "T+0")
|
|
93
|
+
return simTimestamp(start);
|
|
94
|
+
const match = /^T\+(\d+)(m|h|d)$/.exec(offset);
|
|
95
|
+
if (!match) {
|
|
96
|
+
const durationMatch = /^(\d+) sim-(hours|days)$/.exec(offset);
|
|
97
|
+
if (!durationMatch)
|
|
98
|
+
return simTimestamp(start);
|
|
99
|
+
const amount = Number(durationMatch[1]);
|
|
100
|
+
return simTimestamp(start + amount * (durationMatch[2] === "days" ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000));
|
|
101
|
+
}
|
|
102
|
+
const amount = Number(match[1]);
|
|
103
|
+
const unit = match[2];
|
|
104
|
+
const multiplier = unit === "d" ? 24 * 60 * 60 * 1000 : unit === "h" ? 60 * 60 * 1000 : 60 * 1000;
|
|
105
|
+
return simTimestamp(start + amount * multiplier);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=scenario.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EventBus, EventId, JsonObject, LoremEvent, MockSurface } from "@loremcorp/engine";
|
|
2
|
+
import type { MockRuntimeContext } from "@loremcorp/mocks-shared";
|
|
3
|
+
export type ScenarioDuration = `${number} sim-days` | `${number} sim-hours`;
|
|
4
|
+
export interface ScenarioContext {
|
|
5
|
+
readonly runtime: MockRuntimeContext;
|
|
6
|
+
readonly bus: EventBus;
|
|
7
|
+
readonly emitted: ReadonlyMap<string, LoremEvent>;
|
|
8
|
+
}
|
|
9
|
+
export interface ScenarioEventStep {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly at: string;
|
|
12
|
+
readonly source: MockSurface;
|
|
13
|
+
readonly type: string;
|
|
14
|
+
readonly entityId?: string;
|
|
15
|
+
readonly personaId?: string;
|
|
16
|
+
readonly payload: JsonObject;
|
|
17
|
+
readonly causedBy?: readonly string[];
|
|
18
|
+
apply?(context: ScenarioContext, event: LoremEvent): void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface ScenarioBranchStep {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly at: string;
|
|
23
|
+
readonly branch: {
|
|
24
|
+
readonly if: string;
|
|
25
|
+
readonly then: string;
|
|
26
|
+
readonly else: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export type ScenarioTimelineStep = ScenarioEventStep | ScenarioBranchStep;
|
|
30
|
+
export interface ScenarioAssertion {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly at: string;
|
|
33
|
+
readonly description: string;
|
|
34
|
+
check(events: readonly LoremEvent[], context: ScenarioContext): boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ScenarioDefinition {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly company: "halcyon";
|
|
39
|
+
readonly duration: ScenarioDuration;
|
|
40
|
+
readonly pathology: string;
|
|
41
|
+
readonly mocks: readonly MockSurface[];
|
|
42
|
+
readonly heroPersonaIds: readonly string[];
|
|
43
|
+
readonly timeline: readonly ScenarioTimelineStep[];
|
|
44
|
+
readonly assertions: readonly ScenarioAssertion[];
|
|
45
|
+
readonly rubric: Readonly<Record<string, number>>;
|
|
46
|
+
}
|
|
47
|
+
export interface AssertionResult {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly passed: boolean;
|
|
50
|
+
readonly description: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ScenarioRunResult {
|
|
53
|
+
readonly scenarioId: string;
|
|
54
|
+
readonly eventIds: readonly EventId[];
|
|
55
|
+
readonly assertions: readonly AssertionResult[];
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loremcorp/scenarios",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scenario definitions and executor for LoremCorp synthetic companies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/danielthedm/loremcorp.git",
|
|
10
|
+
"directory": "packages/scenarios"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/danielthedm/loremcorp#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/danielthedm/loremcorp/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/**/*.js",
|
|
21
|
+
"dist/**/*.d.ts",
|
|
22
|
+
"!dist/**/*.test.js",
|
|
23
|
+
"!dist/**/*.test.d.ts",
|
|
24
|
+
"!dist/**/*.map"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@loremcorp/mocks-shared": "0.1.0",
|
|
34
|
+
"@loremcorp/personas": "0.1.0",
|
|
35
|
+
"@loremcorp/engine": "0.1.0",
|
|
36
|
+
"@loremcorp/company-halcyon": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -b",
|
|
43
|
+
"test": "node --test dist/*.test.js"
|
|
44
|
+
}
|
|
45
|
+
}
|