@nikelyh/offscript 0.0.0-stage → 1.0.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 (161) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +148 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/dist/packages/application/assign-investigation/execute.js +34 -0
  5. package/dist/packages/application/assign-investigation/ports.js +1 -0
  6. package/dist/packages/application/assign-investigation/resolve-context.js +41 -0
  7. package/dist/packages/application/check-target.js +4 -0
  8. package/dist/packages/application/complete-investigation/execute.js +54 -0
  9. package/dist/packages/application/complete-investigation/ports.js +1 -0
  10. package/dist/packages/application/complete-investigation/resolve-context.js +48 -0
  11. package/dist/packages/application/configure-simulation/execute.js +29 -0
  12. package/dist/packages/application/configure-simulation/ports.js +1 -0
  13. package/dist/packages/application/configure-simulation/profiles.js +16 -0
  14. package/dist/packages/application/delete-configured-simulation.js +11 -0
  15. package/dist/packages/application/errors.js +8 -0
  16. package/dist/packages/application/index.js +11 -0
  17. package/dist/packages/application/outbox/dispatch.js +48 -0
  18. package/dist/packages/application/outbox/ports.js +16 -0
  19. package/dist/packages/application/recover-simulation/execute.js +31 -0
  20. package/dist/packages/application/recover-simulation/ports.js +1 -0
  21. package/dist/packages/application/request-investigation/execute.js +38 -0
  22. package/dist/packages/application/request-investigation/ports.js +1 -0
  23. package/dist/packages/application/request-investigation/resolve-context.js +41 -0
  24. package/dist/packages/browser/browser-manager.js +91 -0
  25. package/dist/packages/browser/dom-snapshot.js +186 -0
  26. package/dist/packages/browser/index.js +5 -0
  27. package/dist/packages/browser/scope-guard.js +73 -0
  28. package/dist/packages/browser/session.js +209 -0
  29. package/dist/packages/browser/tools/browser-tools.js +192 -0
  30. package/dist/packages/browser/types.js +1 -0
  31. package/dist/packages/contracts/errors.js +8 -0
  32. package/dist/packages/contracts/event-envelope.js +28 -0
  33. package/dist/packages/contracts/event-validation.js +44 -0
  34. package/dist/packages/contracts/events.js +1 -0
  35. package/dist/packages/contracts/finding-assessment.js +128 -0
  36. package/dist/packages/contracts/index.js +15 -0
  37. package/dist/packages/contracts/investigation-events.js +43 -0
  38. package/dist/packages/contracts/investigation-summary.js +60 -0
  39. package/dist/packages/contracts/mappers.js +17 -0
  40. package/dist/packages/contracts/observation-summary.js +25 -0
  41. package/dist/packages/contracts/semantic-events.js +41 -0
  42. package/dist/packages/contracts/serialization.js +18 -0
  43. package/dist/packages/contracts/simulation-configuration.js +17 -0
  44. package/dist/packages/contracts/simulation-execution.js +62 -0
  45. package/dist/packages/contracts/simulation-results.js +52 -0
  46. package/dist/packages/contracts/simulation-setup.js +60 -0
  47. package/dist/packages/contracts/summaries.js +42 -0
  48. package/dist/packages/contracts/target-connection.js +36 -0
  49. package/dist/packages/contracts/validation.js +42 -0
  50. package/dist/packages/domain/agent.js +23 -0
  51. package/dist/packages/domain/environment.js +1 -0
  52. package/dist/packages/domain/errors.js +14 -0
  53. package/dist/packages/domain/identity.js +16 -0
  54. package/dist/packages/domain/index.js +10 -0
  55. package/dist/packages/domain/investigation-result.js +19 -0
  56. package/dist/packages/domain/investigation.js +70 -0
  57. package/dist/packages/domain/observation.js +19 -0
  58. package/dist/packages/domain/profile.js +19 -0
  59. package/dist/packages/domain/simulation.js +61 -0
  60. package/dist/packages/domain/timestamp.js +9 -0
  61. package/dist/packages/orchestration/browser-exploration-factory.js +135 -0
  62. package/dist/packages/orchestration/controlled-demo-factory.js +26 -0
  63. package/dist/packages/orchestration/deterministic-browser-runner.js +92 -0
  64. package/dist/packages/orchestration/deterministic-runner.js +69 -0
  65. package/dist/packages/orchestration/event-bridge.js +27 -0
  66. package/dist/packages/orchestration/guard-inference.js +44 -0
  67. package/dist/packages/orchestration/index.js +7 -0
  68. package/dist/packages/orchestration/investigation-reaction.js +53 -0
  69. package/dist/packages/orchestration/investigation-store.js +96 -0
  70. package/dist/packages/orchestration/memory-outbox.js +63 -0
  71. package/dist/packages/orchestration/multi-mode-factory.js +12 -0
  72. package/dist/packages/orchestration/outbox-publisher.js +13 -0
  73. package/dist/packages/orchestration/session-storage.js +52 -0
  74. package/dist/packages/orchestration/session.js +176 -0
  75. package/dist/packages/orchestration/situations.js +26 -0
  76. package/dist/packages/orchestration/state.js +32 -0
  77. package/dist/packages/orchestration/tools/arguments.js +13 -0
  78. package/dist/packages/orchestration/tools/browser-exploration-tools.js +154 -0
  79. package/dist/packages/orchestration/tools/browser-interaction-tools.js +74 -0
  80. package/dist/packages/orchestration/tools/exploration-tools.js +119 -0
  81. package/dist/packages/persistence/codecs/investigation.js +30 -0
  82. package/dist/packages/persistence/codecs/simulation.js +69 -0
  83. package/dist/packages/persistence/codecs/values.js +24 -0
  84. package/dist/packages/persistence/connection.js +29 -0
  85. package/dist/packages/persistence/errors.js +8 -0
  86. package/dist/packages/persistence/index.js +22 -0
  87. package/dist/packages/persistence/migrate.js +40 -0
  88. package/dist/packages/persistence/migrations/0000_initial_domain_storage.sql +92 -0
  89. package/dist/packages/persistence/migrations/0001_transactional_outbox.sql +26 -0
  90. package/dist/packages/persistence/migrations/0002_simulation_configuration.sql +7 -0
  91. package/dist/packages/persistence/migrations/meta/0000_snapshot.json +637 -0
  92. package/dist/packages/persistence/migrations/meta/0001_snapshot.json +812 -0
  93. package/dist/packages/persistence/migrations/meta/0002_snapshot.json +859 -0
  94. package/dist/packages/persistence/migrations/meta/_journal.json +27 -0
  95. package/dist/packages/persistence/outbox/committer.js +54 -0
  96. package/dist/packages/persistence/outbox/store.js +66 -0
  97. package/dist/packages/persistence/repositories/configurations.js +41 -0
  98. package/dist/packages/persistence/repositories/delete-configuration.js +20 -0
  99. package/dist/packages/persistence/repositories/investigation-writes.js +72 -0
  100. package/dist/packages/persistence/repositories/investigations.js +27 -0
  101. package/dist/packages/persistence/repositories/observations.js +36 -0
  102. package/dist/packages/persistence/repositories/simulation-writes.js +39 -0
  103. package/dist/packages/persistence/repositories/simulations.js +23 -0
  104. package/dist/packages/persistence/schema/configurations.js +10 -0
  105. package/dist/packages/persistence/schema/index.js +5 -0
  106. package/dist/packages/persistence/schema/investigations.js +53 -0
  107. package/dist/packages/persistence/schema/observations.js +17 -0
  108. package/dist/packages/persistence/schema/outbox.js +26 -0
  109. package/dist/packages/persistence/schema/simulations.js +29 -0
  110. package/dist/packages/projects/index.js +2 -0
  111. package/dist/packages/projects/inspect.js +45 -0
  112. package/dist/packages/projects/suggest-target.js +34 -0
  113. package/dist/packages/projects/target-probe.js +61 -0
  114. package/dist/packages/runtime/deadline.js +11 -0
  115. package/dist/packages/runtime/evidence-store.js +76 -0
  116. package/dist/packages/runtime/execution-coordinator.js +223 -0
  117. package/dist/packages/runtime/execution-settlement.js +50 -0
  118. package/dist/packages/runtime/execution-types.js +1 -0
  119. package/dist/packages/runtime/host-config.js +26 -0
  120. package/dist/packages/runtime/host-storage.js +46 -0
  121. package/dist/packages/runtime/host.js +2 -0
  122. package/dist/packages/runtime/index.js +1 -0
  123. package/dist/packages/runtime/local-host.js +280 -0
  124. package/dist/packages/runtime/outbox-dispatcher.js +61 -0
  125. package/dist/packages/runtime/session-registry.js +42 -0
  126. package/dist/packages/server/configuration-routes.js +85 -0
  127. package/dist/packages/server/contracts.js +13 -0
  128. package/dist/packages/server/index.js +2 -0
  129. package/dist/packages/server/security.js +38 -0
  130. package/dist/packages/server/server.js +44 -0
  131. package/dist/packages/server/studio-assets.js +44 -0
  132. package/dist/packages/server/studio-session.js +28 -0
  133. package/dist/scripts/server/access-file.js +21 -0
  134. package/dist/scripts/server/cli-results.js +126 -0
  135. package/dist/scripts/server/open-browser.js +13 -0
  136. package/dist/scripts/server/run.js +76 -0
  137. package/dist/scripts/server/startup-error.js +17 -0
  138. package/dist/scripts/server/studio-entry-file.js +22 -0
  139. package/dist/studio/assets/index-Dh5KHl3J.css +1 -0
  140. package/dist/studio/assets/index-uXpu7a13.js +9 -0
  141. package/dist/studio/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  142. package/dist/studio/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  143. package/dist/studio/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  144. package/dist/studio/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  145. package/dist/studio/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  146. package/dist/studio/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
  147. package/dist/studio/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
  148. package/dist/studio/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
  149. package/dist/studio/brand/logo-perspectives.svg +1 -0
  150. package/dist/studio/favicon.svg +1 -0
  151. package/dist/studio/index.html +14 -0
  152. package/dist/studio/licenses/jetbrains-mono.txt +93 -0
  153. package/dist/studio/licenses/space-grotesk.txt +93 -0
  154. package/dist/studio/participants/badger.png +0 -0
  155. package/dist/studio/participants/compass.png +0 -0
  156. package/dist/studio/participants/fox.png +0 -0
  157. package/dist/studio/participants/owl.png +0 -0
  158. package/dist/studio/participants/raven.png +0 -0
  159. package/dist/studio/participants/sparrow.png +0 -0
  160. package/offscript.mjs +104 -0
  161. package/package.json +100 -4
@@ -0,0 +1,74 @@
1
+ import { argumentsObject, argumentText } from "./arguments.js";
2
+ export function browserInteractionTools(tool, browserSession, onActivity) {
3
+ const text = { type: "string", minLength: 1 };
4
+ return [
5
+ tool("browser_navigate", "Navigate to an authorized URL or path in the target web application.", { url: text }, async (value) => {
6
+ const args = argumentsObject(value, ["url"]);
7
+ const url = argumentText(args.url);
8
+ onActivity?.("navigate", url);
9
+ const result = await browserSession.navigate(url);
10
+ if (!result.success)
11
+ throw new Error(result.error || "Navigation failed.");
12
+ return { currentUrl: result.url };
13
+ }),
14
+ tool("browser_inspect", "Inspect current page DOM and accessibility tree, returning visible text, title, and interactive element handles.", {}, async () => {
15
+ onActivity?.("inspect");
16
+ const snapshot = await browserSession.inspect();
17
+ return {
18
+ url: snapshot.url,
19
+ title: snapshot.title,
20
+ textSummary: snapshot.textSummary,
21
+ ariaSnapshot: snapshot.ariaSnapshot,
22
+ elements: snapshot.elements.map((el) => ({
23
+ ref: el.ref,
24
+ role: el.role,
25
+ name: el.name,
26
+ text: el.text,
27
+ disabled: el.disabled,
28
+ ...(el.type ? { type: el.type } : {}),
29
+ ...(el.placeholder ? { placeholder: el.placeholder } : {}),
30
+ ...(el.checked !== undefined ? { checked: el.checked } : {}),
31
+ ...(el.expanded !== undefined ? { expanded: el.expanded } : {}),
32
+ })),
33
+ };
34
+ }),
35
+ tool("browser_click", "Click an interactive element on the current page using its opaque reference (e.g. elem_1).", { ref: text }, async (value) => {
36
+ const args = argumentsObject(value, ["ref"]);
37
+ const ref = argumentText(args.ref);
38
+ onActivity?.("click", ref);
39
+ const result = await browserSession.click(ref);
40
+ if (!result.success)
41
+ throw new Error(result.error || "Click failed.");
42
+ return { currentUrl: result.url };
43
+ }),
44
+ tool("browser_type", "Type text into an input or editable element using its opaque reference.", { ref: text, text: { type: "string" } }, async (value) => {
45
+ const args = argumentsObject(value, ["ref", "text"]);
46
+ const ref = argumentText(args.ref);
47
+ const inputContent = typeof args.text === "string" ? args.text : "";
48
+ onActivity?.("type", ref);
49
+ const result = await browserSession.type(ref, inputContent);
50
+ if (!result.success)
51
+ throw new Error(result.error || "Type failed.");
52
+ return { currentUrl: result.url };
53
+ }),
54
+ tool("browser_scroll", "Scroll the page up or down to reveal hidden content or elements below the fold.", { direction: { type: "string", enum: ["up", "down"] } }, async (value) => {
55
+ const args = argumentsObject(value, ["direction"]);
56
+ const dir = args.direction === "up" ? "up" : "down";
57
+ onActivity?.("scroll", dir);
58
+ const result = await browserSession.scroll(dir);
59
+ if (!result.success)
60
+ throw new Error(result.error || "Scroll failed.");
61
+ return { currentUrl: result.url };
62
+ }),
63
+ tool("browser_select", "Select an option on a select or dropdown element using its opaque reference (e.g. elem_1) and option value.", { ref: text, value: text }, async (value) => {
64
+ const args = argumentsObject(value, ["ref", "value"]);
65
+ const ref = argumentText(args.ref);
66
+ const val = argumentText(args.value);
67
+ onActivity?.("select", `${ref}=${val}`);
68
+ const result = await browserSession.select(ref, val);
69
+ if (!result.success)
70
+ throw new Error(result.error || "Select failed.");
71
+ return { currentUrl: result.url };
72
+ }),
73
+ ];
74
+ }
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createObservation, evidenceId, investigationId, observationId } from "../../domain/index.js";
3
+ import { parseSemanticEvent } from "../../contracts/index.js";
4
+ import { executeCompleteInvestigation, executeRequestInvestigation } from "../../application/index.js";
5
+ import { sessionStorage } from "../session-storage.js";
6
+ import { argumentsObject, argumentText } from "./arguments.js";
7
+ export function explorationTools(state, id, publish, storage = sessionStorage(state)) {
8
+ function tool(name, description, properties, invoke) {
9
+ return {
10
+ type: "function", name, description, strict: true,
11
+ parameters: { type: "object", properties, required: Object.keys(properties), additionalProperties: false },
12
+ async invoke(args) {
13
+ try {
14
+ if (state.closed || !state.permissions.get(id)?.has(name))
15
+ throw new Error("Tool action is not allowed in this session.");
16
+ return await invoke(args);
17
+ }
18
+ catch (cause) {
19
+ state.fail(`tool:${name}`, id, cause);
20
+ return { error: "Tool action rejected or failed. No permission was granted by this response." };
21
+ }
22
+ },
23
+ };
24
+ }
25
+ const text = { type: "string", minLength: 1 };
26
+ const tools = [
27
+ tool("inspect_application", "Inspect the local simulated cart before or after reload. This is not a real browser.", {
28
+ phase: { type: "string", enum: ["before_reload", "after_reload"] },
29
+ }, async (value) => {
30
+ const args = argumentsObject(value, ["phase"]);
31
+ if (args.phase !== "before_reload" && args.phase !== "after_reload")
32
+ throw new Error("Unknown inspection phase.");
33
+ return { location: "/cart", itemCount: args.phase === "before_reload" ? 1 : 0, source: "simulated-fixture" };
34
+ }),
35
+ tool("record_observation", "Record noticed behavior as untrusted observation data, without confirming a defect.", {
36
+ description: text, location: text,
37
+ }, async (value) => {
38
+ const args = argumentsObject(value, ["description", "location"]);
39
+ const desc = argumentText(args.description);
40
+ const loc = argumentText(args.location);
41
+ for (const existing of state.observations.values()) {
42
+ if (existing.simulationId === state.simulation.id && existing.agentId === id &&
43
+ existing.description === desc && existing.context.location === loc) {
44
+ return { observationId: existing.id, evidenceId: existing.evidenceIds[0] ?? "" };
45
+ }
46
+ }
47
+ const eid = evidenceId(`ev-${randomUUID()}`);
48
+ const observation = createObservation({
49
+ id: observationId(randomUUID()), simulationId: state.simulation.id, agentId: id,
50
+ observedAt: new Date().toISOString(), description: desc,
51
+ context: { location: loc }, evidenceIds: [eid],
52
+ });
53
+ if (await storage.observations.createObservation(observation) !== "created")
54
+ throw new Error("Observation identity already exists.");
55
+ state.observations.set(observation.id, observation);
56
+ publish(parseSemanticEvent({
57
+ eventId: randomUUID(), type: "observation.created", schemaVersion: 1, occurredAt: observation.observedAt,
58
+ simulationId: state.simulation.id, origin: { kind: "agent", agentId: id },
59
+ correlationId: state.simulation.id, payload: { observation },
60
+ }));
61
+ return { observationId: observation.id, evidenceId: eid };
62
+ }),
63
+ tool("request_investigation", "Request collaborative examination of existing observations, without assigning or confirming findings.", {
64
+ hypothesis: text, observationIds: { type: "array", items: text, minItems: 1, uniqueItems: true },
65
+ }, async (value) => {
66
+ const args = argumentsObject(value, ["hypothesis", "observationIds"]);
67
+ if (!Array.isArray(args.observationIds))
68
+ throw new Error("Expected observation identities.");
69
+ const references = Array.from(args.observationIds, (entry) => observationId(argumentText(entry)));
70
+ const result = await executeRequestInvestigation({
71
+ investigationId: investigationId(randomUUID()), simulationId: state.simulation.id, requestedBy: id,
72
+ observationIds: references, hypothesis: argumentText(args.hypothesis), requestedAt: new Date().toISOString(),
73
+ eventId: randomUUID(), correlationId: state.simulation.id,
74
+ }, {
75
+ simulations: { async findSimulation(key) { return key === state.simulation.id ? state.simulation : undefined; } },
76
+ observations: storage.observations,
77
+ investigations: storage.investigations,
78
+ });
79
+ return { investigationId: result.investigation.id, publication: result.publication.status };
80
+ }),
81
+ tool("complete_investigation", "Record an assessment outcome for an assigned investigation with explanation and evidence references.", {
82
+ investigationId: text,
83
+ outcome: { type: "string", enum: ["supported", "contradicted", "inconclusive"] },
84
+ explanation: text,
85
+ evidenceIds: { type: "array", items: text },
86
+ }, async (value) => {
87
+ const args = argumentsObject(value, ["investigationId", "outcome", "explanation", "evidenceIds"]);
88
+ if (args.outcome !== "supported" && args.outcome !== "contradicted" && args.outcome !== "inconclusive") {
89
+ throw new Error("Invalid investigation outcome.");
90
+ }
91
+ const evidence = Array.isArray(args.evidenceIds)
92
+ ? args.evidenceIds.map((item) => argumentText(item))
93
+ : [];
94
+ const result = await executeCompleteInvestigation({
95
+ investigationId: investigationId(argumentText(args.investigationId)),
96
+ simulationId: state.simulation.id,
97
+ completedBy: id,
98
+ result: {
99
+ outcome: args.outcome,
100
+ explanation: argumentText(args.explanation),
101
+ evidenceIds: evidence.map(evidenceId),
102
+ },
103
+ origin: { kind: "agent", agentId: id },
104
+ completedAt: new Date().toISOString(),
105
+ eventId: randomUUID(),
106
+ correlationId: state.simulation.id,
107
+ }, {
108
+ simulations: { async findSimulation(key) { return key === state.simulation.id ? state.simulation : undefined; } },
109
+ investigations: storage.investigations,
110
+ });
111
+ return {
112
+ investigationId: result.investigation.id,
113
+ outcome: result.investigation.lifecycle.result.outcome,
114
+ status: result.investigation.lifecycle.status,
115
+ };
116
+ }),
117
+ ];
118
+ return tools.filter((entry) => state.permissions.get(id)?.has(entry.name));
119
+ }
@@ -0,0 +1,30 @@
1
+ import { asc, eq } from "drizzle-orm";
2
+ import { parseInvestigationSummary } from "../../contracts/index.js";
3
+ import { investigations, investigationAgents, investigationObservations } from "../schema/index.js";
4
+ import { PersistenceError } from "../errors.js";
5
+ import { json, stored } from "./values.js";
6
+ export function readInvestigation(db, id) {
7
+ const row = db.select().from(investigations).where(eq(investigations.id, id)).get();
8
+ if (!row)
9
+ return undefined;
10
+ const links = db.select().from(investigationObservations).where(eq(investigationObservations.investigationId, id)).orderBy(asc(investigationObservations.position)).all();
11
+ const agents = db.select().from(investigationAgents).where(eq(investigationAgents.investigationId, id)).orderBy(asc(investigationAgents.position)).all();
12
+ return stored(() => {
13
+ if (links.some((link) => link.simulationId !== row.simulationId) || agents.some((agent) => agent.simulationId !== row.simulationId)) {
14
+ throw new PersistenceError("INVALID_STORED_DATA", "Investigation links cross simulation scope.");
15
+ }
16
+ if (row.status === "requested" && agents.length)
17
+ throw new PersistenceError("INVALID_STORED_DATA", "Requested investigation has assignees.");
18
+ const lifecycle = row.status === "requested" ? { status: row.status } : {
19
+ status: row.status, agentIds: agents.map((agent) => agent.agentId), assignedAt: row.assignedAt,
20
+ ...(row.status === "completed" ? { completedAt: row.completedAt, completedBy: row.completedBy, result: row.result === null ? null : json(row.result) } : {}),
21
+ };
22
+ const investigation = parseInvestigationSummary({
23
+ id: row.id, simulationId: row.simulationId, requestedBy: row.requestedBy, hypothesis: row.hypothesis,
24
+ requestedAt: row.requestedAt, observationIds: links.map((link) => link.observationId), lifecycle,
25
+ });
26
+ if (!Number.isSafeInteger(row.revision) || row.revision < 0)
27
+ throw new PersistenceError("INVALID_STORED_DATA", "Invalid storage revision.");
28
+ return Object.freeze({ investigation, revision: row.revision });
29
+ });
30
+ }
@@ -0,0 +1,69 @@
1
+ import { createAgent, createAgentProfile, createSimulation, transitionSimulation, updateSimulationAgent } from "../../domain/index.js";
2
+ import { parseAgentSummary, parseSimulationSummary } from "../../contracts/index.js";
3
+ import { agents, simulations } from "../schema/index.js";
4
+ import { PersistenceError } from "../errors.js";
5
+ import { json, object, stored, text } from "./values.js";
6
+ function profile(value) {
7
+ const input = object(value, ["name", "personality", "goals", "capabilities"]);
8
+ if (!Array.isArray(input.goals) || !Array.isArray(input.capabilities)) {
9
+ throw new PersistenceError("INVALID_STORED_DATA", "Stored profile goals/capabilities must be arrays.");
10
+ }
11
+ return createAgentProfile({
12
+ name: text(input.name), personality: text(input.personality),
13
+ goals: Array.from(input.goals, (goal) => ({ description: text(object(goal, ["description"]).description) })),
14
+ capabilities: Array.from(input.capabilities, (capability) => text(capability)),
15
+ });
16
+ }
17
+ function lifecycle(row) {
18
+ return row.reason === null ? { status: row.status } : { status: row.status, reason: row.reason };
19
+ }
20
+ export function restoreSimulation(row, members) {
21
+ return stored(() => {
22
+ const summary = parseSimulationSummary({
23
+ id: row.id, environment: { kind: row.environment, deployment: row.deployment },
24
+ lifecycle: lifecycle(row), agentIds: members.map((member) => member.id),
25
+ });
26
+ const parsed = members.map((member) => {
27
+ if (member.simulationId !== row.id)
28
+ throw new PersistenceError("INVALID_STORED_DATA", "Agent belongs to another simulation.");
29
+ const parsedProfile = profile(json(member.profile));
30
+ return { profile: parsedProfile, summary: parseAgentSummary({
31
+ id: member.id, simulationId: row.id, name: parsedProfile.name, lifecycle: lifecycle(member),
32
+ }) };
33
+ });
34
+ let simulation = createSimulation(summary.id, summary.environment, parsed.map((member) => createAgent(member.summary.id, member.profile)));
35
+ const status = summary.lifecycle.status;
36
+ if (status !== "created") {
37
+ if (status === "cancelled" && parsed.length === 0)
38
+ return transitionSimulation(simulation, { status: "cancelled" });
39
+ simulation = transitionSimulation(simulation, { status: "running" });
40
+ for (const member of parsed) {
41
+ const target = member.summary.lifecycle;
42
+ if (target.status === "ready")
43
+ continue;
44
+ if (target.status !== "cancelled")
45
+ simulation = updateSimulationAgent(simulation, member.summary.id, { status: "running" });
46
+ if (target.status !== "running")
47
+ simulation = updateSimulationAgent(simulation, member.summary.id, target);
48
+ }
49
+ if (status !== "running")
50
+ simulation = transitionSimulation(simulation, summary.lifecycle);
51
+ }
52
+ if (parsed.some((member, index) => JSON.stringify(member.summary.lifecycle) !== JSON.stringify(simulation.agents[index]?.lifecycle))) {
53
+ throw new PersistenceError("INVALID_STORED_DATA", "Simulation and participation lifecycles are inconsistent.");
54
+ }
55
+ return simulation;
56
+ });
57
+ }
58
+ export function simulationRows(simulation) {
59
+ const row = {
60
+ id: simulation.id, environment: simulation.environment.kind, deployment: simulation.environment.deployment,
61
+ status: simulation.lifecycle.status, reason: simulation.lifecycle.status === "failed" ? simulation.lifecycle.reason : null,
62
+ };
63
+ const members = simulation.agents.map((agent, position) => ({
64
+ simulationId: simulation.id, id: agent.id, position, profile: JSON.stringify(agent.profile),
65
+ status: agent.lifecycle.status, reason: agent.lifecycle.status === "failed" ? agent.lifecycle.reason : null,
66
+ }));
67
+ restoreSimulation(row, members);
68
+ return { row, members };
69
+ }
@@ -0,0 +1,24 @@
1
+ import { PersistenceError } from "../errors.js";
2
+ export function object(value, fields) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
4
+ Reflect.ownKeys(value).some((key) => typeof key !== "string" || !fields.includes(key))) {
5
+ throw new PersistenceError("INVALID_STORED_DATA", "Invalid stored object shape.");
6
+ }
7
+ return value;
8
+ }
9
+ export function text(value) {
10
+ if (typeof value !== "string" || !value.trim())
11
+ throw new PersistenceError("INVALID_STORED_DATA", "Expected stored nonblank text.");
12
+ return value;
13
+ }
14
+ export function json(value) { return JSON.parse(value); }
15
+ export function stored(read) {
16
+ try {
17
+ return read();
18
+ }
19
+ catch (cause) {
20
+ if (cause instanceof PersistenceError)
21
+ throw cause;
22
+ throw new PersistenceError("INVALID_STORED_DATA", "Stored data failed domain/contract validation.", { cause });
23
+ }
24
+ }
@@ -0,0 +1,29 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { drizzle } from "drizzle-orm/better-sqlite3";
5
+ export function openConnection(options) {
6
+ if (!options.projectRoot.trim())
7
+ throw new Error("An explicit project root is required.");
8
+ if (options.dataDirectory !== undefined && !options.dataDirectory.trim())
9
+ throw new Error("Data directory must not be blank.");
10
+ const timeout = options.busyTimeoutMs ?? 2_500;
11
+ if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 30_000)
12
+ throw new Error("Busy timeout must be between 0 and 30000 ms.");
13
+ const dataDirectory = resolve(options.projectRoot, options.dataDirectory ?? ".offscript");
14
+ mkdirSync(dataDirectory, { recursive: true });
15
+ const databasePath = resolve(dataDirectory, "offscript.sqlite");
16
+ const client = new Database(databasePath, { timeout });
17
+ try {
18
+ client.pragma("foreign_keys = ON");
19
+ if (client.pragma("journal_mode = WAL", { simple: true }) !== "wal")
20
+ throw new Error("SQLite WAL mode could not be enabled.");
21
+ client.pragma("synchronous = FULL");
22
+ client.pragma(`busy_timeout = ${timeout}`);
23
+ return { client, db: drizzle(client), databasePath };
24
+ }
25
+ catch (error) {
26
+ client.close();
27
+ throw error;
28
+ }
29
+ }
@@ -0,0 +1,8 @@
1
+ export class PersistenceError extends Error {
2
+ code;
3
+ constructor(code, message, options) {
4
+ super(message, options);
5
+ this.name = "PersistenceError";
6
+ this.code = code;
7
+ }
8
+ }
@@ -0,0 +1,22 @@
1
+ import { openConnection } from "./connection.js";
2
+ import { migrateDatabase } from "./migrate.js";
3
+ import { simulationRepository } from "./repositories/simulations.js";
4
+ import { observationRepository } from "./repositories/observations.js";
5
+ import { investigationRepository } from "./repositories/investigations.js";
6
+ import { investigationCommitter } from "./outbox/committer.js";
7
+ import { outboxStore } from "./outbox/store.js";
8
+ import { configurationRepository } from "./repositories/configurations.js";
9
+ export function openPersistence(options) {
10
+ const connection = openConnection(options);
11
+ return Object.freeze({
12
+ databasePath: connection.databasePath,
13
+ migrate() { migrateDatabase(connection.db); },
14
+ simulations: simulationRepository(connection.db), observations: observationRepository(connection.db),
15
+ investigations: Object.freeze({ ...investigationRepository(connection.db), ...investigationCommitter(connection.db) }),
16
+ outbox: outboxStore(connection.db),
17
+ configurations: configurationRepository(connection.db),
18
+ close() { if (connection.client.open)
19
+ connection.client.close(); },
20
+ });
21
+ }
22
+ export { PersistenceError } from "./errors.js";
@@ -0,0 +1,40 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { sql } from "drizzle-orm";
3
+ import { readMigrationFiles } from "drizzle-orm/migrator";
4
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
5
+ import { PersistenceError } from "./errors.js";
6
+ const history = sqliteTable("__offscript_migrations", {
7
+ version: integer("version").primaryKey(), hash: text("hash").notNull(), createdAt: integer("created_at").notNull(),
8
+ });
9
+ const defaultFolder = fileURLToPath(new URL("./migrations", import.meta.url));
10
+ export function migrateDatabase(db, folder = defaultFolder) {
11
+ try {
12
+ const migrations = readMigrationFiles({ migrationsFolder: folder });
13
+ db.transaction((tx) => {
14
+ tx.run(sql `CREATE TABLE IF NOT EXISTS __offscript_migrations (version INTEGER PRIMARY KEY, hash TEXT NOT NULL, created_at INTEGER NOT NULL)`);
15
+ const applied = tx.select().from(history).orderBy(history.version).all();
16
+ if (applied.length > migrations.length)
17
+ throw new PersistenceError("INCOMPATIBLE_SCHEMA", "Database has migrations unknown to this version.");
18
+ for (const [index, row] of applied.entries()) {
19
+ const migration = migrations[index];
20
+ if (!migration || row.version !== index + 1 || row.hash !== migration.hash || row.createdAt !== migration.folderMillis) {
21
+ throw new PersistenceError("INCOMPATIBLE_SCHEMA", "Migration history or checksum does not match this version.");
22
+ }
23
+ }
24
+ for (let index = applied.length; index < migrations.length; index++) {
25
+ const migration = migrations[index];
26
+ if (!migration)
27
+ throw new PersistenceError("MIGRATION_FAILED", "Missing migration metadata.");
28
+ for (const statement of migration.sql)
29
+ if (statement.trim())
30
+ tx.run(sql.raw(statement));
31
+ tx.insert(history).values({ version: index + 1, hash: migration.hash, createdAt: migration.folderMillis }).run();
32
+ }
33
+ }, { behavior: "immediate" });
34
+ }
35
+ catch (cause) {
36
+ if (cause instanceof PersistenceError)
37
+ throw cause;
38
+ throw new PersistenceError("MIGRATION_FAILED", "Could not apply storage migrations; transaction rolled back.", { cause });
39
+ }
40
+ }
@@ -0,0 +1,92 @@
1
+ CREATE TABLE `agents` (
2
+ `simulation_id` text NOT NULL,
3
+ `id` text NOT NULL,
4
+ `position` integer NOT NULL,
5
+ `profile` text NOT NULL,
6
+ `status` text NOT NULL,
7
+ `reason` text,
8
+ PRIMARY KEY(`simulation_id`, `id`),
9
+ FOREIGN KEY (`simulation_id`) REFERENCES `simulations`(`id`) ON UPDATE no action ON DELETE no action,
10
+ CONSTRAINT "agent_nonnegative_position" CHECK("agents"."position" >= 0),
11
+ CONSTRAINT "agent_profile_json" CHECK(json_valid("agents"."profile")),
12
+ CONSTRAINT "agent_status" CHECK("agents"."status" in ('ready','running','completed','failed','cancelled')),
13
+ CONSTRAINT "agent_reason" CHECK(("agents"."status" = 'failed' and "agents"."reason" is not null and length(trim("agents"."reason")) > 0) or ("agents"."status" != 'failed' and "agents"."reason" is null))
14
+ );
15
+ --> statement-breakpoint
16
+ CREATE UNIQUE INDEX `agent_position` ON `agents` (`simulation_id`,`position`);--> statement-breakpoint
17
+ CREATE TABLE `investigation_agents` (
18
+ `investigation_id` text NOT NULL,
19
+ `simulation_id` text NOT NULL,
20
+ `agent_id` text NOT NULL,
21
+ `position` integer NOT NULL,
22
+ PRIMARY KEY(`investigation_id`, `agent_id`),
23
+ FOREIGN KEY (`investigation_id`,`simulation_id`) REFERENCES `investigations`(`id`,`simulation_id`) ON UPDATE no action ON DELETE no action,
24
+ FOREIGN KEY (`simulation_id`,`agent_id`) REFERENCES `agents`(`simulation_id`,`id`) ON UPDATE no action ON DELETE no action,
25
+ CONSTRAINT "investigation_agent_nonnegative_position" CHECK("investigation_agents"."position" >= 0)
26
+ );
27
+ --> statement-breakpoint
28
+ CREATE UNIQUE INDEX `investigation_agent_position` ON `investigation_agents` (`investigation_id`,`position`);--> statement-breakpoint
29
+ CREATE TABLE `investigation_observations` (
30
+ `investigation_id` text NOT NULL,
31
+ `simulation_id` text NOT NULL,
32
+ `observation_id` text NOT NULL,
33
+ `position` integer NOT NULL,
34
+ PRIMARY KEY(`investigation_id`, `observation_id`),
35
+ FOREIGN KEY (`investigation_id`,`simulation_id`) REFERENCES `investigations`(`id`,`simulation_id`) ON UPDATE no action ON DELETE no action,
36
+ FOREIGN KEY (`observation_id`,`simulation_id`) REFERENCES `observations`(`id`,`simulation_id`) ON UPDATE no action ON DELETE no action,
37
+ CONSTRAINT "investigation_observation_nonnegative_position" CHECK("investigation_observations"."position" >= 0)
38
+ );
39
+ --> statement-breakpoint
40
+ CREATE UNIQUE INDEX `investigation_observation_position` ON `investigation_observations` (`investigation_id`,`position`);--> statement-breakpoint
41
+ CREATE TABLE `investigations` (
42
+ `id` text PRIMARY KEY NOT NULL,
43
+ `simulation_id` text NOT NULL,
44
+ `requested_by` text NOT NULL,
45
+ `hypothesis` text NOT NULL,
46
+ `requested_at` text NOT NULL,
47
+ `status` text NOT NULL,
48
+ `assigned_at` text,
49
+ `completed_at` text,
50
+ `completed_by` text,
51
+ `result` text,
52
+ `revision` integer DEFAULT 0 NOT NULL,
53
+ FOREIGN KEY (`simulation_id`) REFERENCES `simulations`(`id`) ON UPDATE no action ON DELETE no action,
54
+ FOREIGN KEY (`simulation_id`,`requested_by`) REFERENCES `agents`(`simulation_id`,`id`) ON UPDATE no action ON DELETE no action,
55
+ FOREIGN KEY (`simulation_id`,`completed_by`) REFERENCES `agents`(`simulation_id`,`id`) ON UPDATE no action ON DELETE no action,
56
+ CONSTRAINT "investigation_revision" CHECK("investigations"."revision" >= 0 and "investigations"."revision" <= 9007199254740991),
57
+ CONSTRAINT "investigation_state" CHECK((
58
+ ("investigations"."status" = 'requested' and "investigations"."assigned_at" is null and "investigations"."completed_at" is null and "investigations"."completed_by" is null and "investigations"."result" is null)
59
+ or ("investigations"."status" = 'assigned' and "investigations"."assigned_at" is not null and "investigations"."completed_at" is null and "investigations"."completed_by" is null and "investigations"."result" is null)
60
+ or ("investigations"."status" = 'completed' and "investigations"."assigned_at" is not null and "investigations"."completed_at" is not null and "investigations"."completed_by" is not null and "investigations"."result" is not null)
61
+ )),
62
+ CONSTRAINT "investigation_result_json" CHECK("investigations"."result" is null or json_valid("investigations"."result"))
63
+ );
64
+ --> statement-breakpoint
65
+ CREATE INDEX `investigation_simulation_status` ON `investigations` (`simulation_id`,`status`,`requested_at`,`id`);--> statement-breakpoint
66
+ CREATE UNIQUE INDEX `investigation_scope` ON `investigations` (`id`,`simulation_id`);--> statement-breakpoint
67
+ CREATE TABLE `observations` (
68
+ `id` text PRIMARY KEY NOT NULL,
69
+ `simulation_id` text NOT NULL,
70
+ `agent_id` text NOT NULL,
71
+ `observed_at` text NOT NULL,
72
+ `description` text NOT NULL,
73
+ `location` text NOT NULL,
74
+ `evidence_ids` text NOT NULL,
75
+ FOREIGN KEY (`simulation_id`) REFERENCES `simulations`(`id`) ON UPDATE no action ON DELETE no action,
76
+ FOREIGN KEY (`simulation_id`,`agent_id`) REFERENCES `agents`(`simulation_id`,`id`) ON UPDATE no action ON DELETE no action,
77
+ CONSTRAINT "observation_evidence_json" CHECK(json_valid("observations"."evidence_ids"))
78
+ );
79
+ --> statement-breakpoint
80
+ CREATE INDEX `observation_simulation_time` ON `observations` (`simulation_id`,`observed_at`,`id`);--> statement-breakpoint
81
+ CREATE UNIQUE INDEX `observation_scope` ON `observations` (`id`,`simulation_id`);--> statement-breakpoint
82
+ CREATE TABLE `simulations` (
83
+ `id` text PRIMARY KEY NOT NULL,
84
+ `environment` text NOT NULL,
85
+ `deployment` text NOT NULL,
86
+ `status` text NOT NULL,
87
+ `reason` text,
88
+ CONSTRAINT "simulation_environment" CHECK("simulations"."environment" = 'web'),
89
+ CONSTRAINT "simulation_deployment" CHECK("simulations"."deployment" in ('local','development','test','staging','production')),
90
+ CONSTRAINT "simulation_status" CHECK("simulations"."status" in ('created','running','completed','failed','cancelled')),
91
+ CONSTRAINT "simulation_reason" CHECK(("simulations"."status" = 'failed' and "simulations"."reason" is not null and length(trim("simulations"."reason")) > 0) or ("simulations"."status" != 'failed' and "simulations"."reason" is null))
92
+ );
@@ -0,0 +1,26 @@
1
+ CREATE TABLE `event_outbox` (
2
+ `sequence` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `event_id` text NOT NULL,
4
+ `simulation_id` text NOT NULL,
5
+ `investigation_id` text NOT NULL,
6
+ `revision` integer NOT NULL,
7
+ `event_json` text NOT NULL,
8
+ `status` text DEFAULT 'pending' NOT NULL,
9
+ `attempts` integer DEFAULT 0 NOT NULL,
10
+ `available_at` integer DEFAULT 0 NOT NULL,
11
+ `lease_token` text,
12
+ `lease_until` integer,
13
+ `accepted_at` integer,
14
+ `last_error` text,
15
+ FOREIGN KEY (`investigation_id`,`simulation_id`) REFERENCES `investigations`(`id`,`simulation_id`) ON UPDATE no action ON DELETE no action,
16
+ CONSTRAINT "outbox_json" CHECK(json_valid("event_outbox"."event_json")),
17
+ CONSTRAINT "outbox_numbers" CHECK("event_outbox"."revision" >= 0 and "event_outbox"."attempts" >= 0 and "event_outbox"."available_at" >= 0),
18
+ CONSTRAINT "outbox_status" CHECK("event_outbox"."status" in ('pending','leased','accepted','failed')),
19
+ CONSTRAINT "outbox_lease" CHECK(("event_outbox"."status" = 'leased' and "event_outbox"."lease_token" is not null and "event_outbox"."lease_until" is not null) or ("event_outbox"."status" != 'leased' and "event_outbox"."lease_token" is null and "event_outbox"."lease_until" is null)),
20
+ CONSTRAINT "outbox_acceptance" CHECK(("event_outbox"."status" = 'accepted' and "event_outbox"."accepted_at" is not null) or ("event_outbox"."status" != 'accepted' and "event_outbox"."accepted_at" is null))
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE UNIQUE INDEX `event_outbox_event_id_unique` ON `event_outbox` (`event_id`);--> statement-breakpoint
24
+ CREATE INDEX `outbox_ready` ON `event_outbox` (`status`,`available_at`,`sequence`);--> statement-breakpoint
25
+ CREATE INDEX `outbox_aggregate_order` ON `event_outbox` (`investigation_id`,`sequence`,`status`);--> statement-breakpoint
26
+ CREATE UNIQUE INDEX `outbox_investigation_revision` ON `event_outbox` (`investigation_id`,`revision`);
@@ -0,0 +1,7 @@
1
+ CREATE TABLE `simulation_configurations` (
2
+ `simulation_id` text PRIMARY KEY NOT NULL,
3
+ `configuration` text NOT NULL,
4
+ FOREIGN KEY (`simulation_id`) REFERENCES `simulations`(`id`) ON UPDATE no action ON DELETE no action,
5
+ CONSTRAINT "configuration_json" CHECK(json_valid("simulation_configurations"."configuration") and json_type("simulation_configurations"."configuration") = 'object'),
6
+ CONSTRAINT "configuration_identity" CHECK(json_type("simulation_configurations"."configuration", '$.simulationId') is 'text' and json_extract("simulation_configurations"."configuration", '$.simulationId') = "simulation_configurations"."simulation_id")
7
+ );