@nikelyh/offscript 0.0.0-stage → 1.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 +201 -0
- package/README.md +183 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/dist/packages/application/assign-investigation/execute.js +34 -0
- package/dist/packages/application/assign-investigation/ports.js +1 -0
- package/dist/packages/application/assign-investigation/resolve-context.js +41 -0
- package/dist/packages/application/check-target.js +4 -0
- package/dist/packages/application/complete-investigation/execute.js +54 -0
- package/dist/packages/application/complete-investigation/ports.js +1 -0
- package/dist/packages/application/complete-investigation/resolve-context.js +48 -0
- package/dist/packages/application/configure-simulation/execute.js +29 -0
- package/dist/packages/application/configure-simulation/ports.js +1 -0
- package/dist/packages/application/configure-simulation/profiles.js +16 -0
- package/dist/packages/application/delete-configured-simulation.js +11 -0
- package/dist/packages/application/errors.js +8 -0
- package/dist/packages/application/index.js +11 -0
- package/dist/packages/application/outbox/dispatch.js +48 -0
- package/dist/packages/application/outbox/ports.js +16 -0
- package/dist/packages/application/recover-simulation/execute.js +31 -0
- package/dist/packages/application/recover-simulation/ports.js +1 -0
- package/dist/packages/application/request-investigation/execute.js +38 -0
- package/dist/packages/application/request-investigation/ports.js +1 -0
- package/dist/packages/application/request-investigation/resolve-context.js +41 -0
- package/dist/packages/browser/browser-manager.js +91 -0
- package/dist/packages/browser/dom-snapshot.js +186 -0
- package/dist/packages/browser/index.js +5 -0
- package/dist/packages/browser/scope-guard.js +73 -0
- package/dist/packages/browser/session.js +209 -0
- package/dist/packages/browser/tools/browser-tools.js +192 -0
- package/dist/packages/browser/types.js +1 -0
- package/dist/packages/contracts/errors.js +8 -0
- package/dist/packages/contracts/event-envelope.js +28 -0
- package/dist/packages/contracts/event-validation.js +44 -0
- package/dist/packages/contracts/events.js +1 -0
- package/dist/packages/contracts/finding-assessment.js +128 -0
- package/dist/packages/contracts/index.js +15 -0
- package/dist/packages/contracts/investigation-events.js +43 -0
- package/dist/packages/contracts/investigation-summary.js +60 -0
- package/dist/packages/contracts/mappers.js +17 -0
- package/dist/packages/contracts/observation-summary.js +25 -0
- package/dist/packages/contracts/semantic-events.js +41 -0
- package/dist/packages/contracts/serialization.js +18 -0
- package/dist/packages/contracts/simulation-configuration.js +17 -0
- package/dist/packages/contracts/simulation-execution.js +62 -0
- package/dist/packages/contracts/simulation-results.js +52 -0
- package/dist/packages/contracts/simulation-setup.js +60 -0
- package/dist/packages/contracts/summaries.js +42 -0
- package/dist/packages/contracts/target-connection.js +36 -0
- package/dist/packages/contracts/validation.js +42 -0
- package/dist/packages/domain/agent.js +23 -0
- package/dist/packages/domain/environment.js +1 -0
- package/dist/packages/domain/errors.js +14 -0
- package/dist/packages/domain/identity.js +16 -0
- package/dist/packages/domain/index.js +10 -0
- package/dist/packages/domain/investigation-result.js +19 -0
- package/dist/packages/domain/investigation.js +70 -0
- package/dist/packages/domain/observation.js +19 -0
- package/dist/packages/domain/profile.js +19 -0
- package/dist/packages/domain/simulation.js +61 -0
- package/dist/packages/domain/timestamp.js +9 -0
- package/dist/packages/orchestration/browser-exploration-factory.js +135 -0
- package/dist/packages/orchestration/controlled-demo-factory.js +26 -0
- package/dist/packages/orchestration/deterministic-browser-runner.js +92 -0
- package/dist/packages/orchestration/deterministic-runner.js +69 -0
- package/dist/packages/orchestration/event-bridge.js +27 -0
- package/dist/packages/orchestration/guard-inference.js +44 -0
- package/dist/packages/orchestration/index.js +7 -0
- package/dist/packages/orchestration/investigation-reaction.js +53 -0
- package/dist/packages/orchestration/investigation-store.js +96 -0
- package/dist/packages/orchestration/memory-outbox.js +63 -0
- package/dist/packages/orchestration/multi-mode-factory.js +12 -0
- package/dist/packages/orchestration/outbox-publisher.js +13 -0
- package/dist/packages/orchestration/session-storage.js +52 -0
- package/dist/packages/orchestration/session.js +176 -0
- package/dist/packages/orchestration/situations.js +26 -0
- package/dist/packages/orchestration/state.js +32 -0
- package/dist/packages/orchestration/tools/arguments.js +13 -0
- package/dist/packages/orchestration/tools/browser-exploration-tools.js +154 -0
- package/dist/packages/orchestration/tools/browser-interaction-tools.js +74 -0
- package/dist/packages/orchestration/tools/exploration-tools.js +119 -0
- package/dist/packages/persistence/codecs/investigation.js +30 -0
- package/dist/packages/persistence/codecs/simulation.js +69 -0
- package/dist/packages/persistence/codecs/values.js +24 -0
- package/dist/packages/persistence/connection.js +29 -0
- package/dist/packages/persistence/errors.js +8 -0
- package/dist/packages/persistence/index.js +22 -0
- package/dist/packages/persistence/migrate.js +40 -0
- package/dist/packages/persistence/migrations/0000_initial_domain_storage.sql +92 -0
- package/dist/packages/persistence/migrations/0001_transactional_outbox.sql +26 -0
- package/dist/packages/persistence/migrations/0002_simulation_configuration.sql +7 -0
- package/dist/packages/persistence/migrations/meta/0000_snapshot.json +637 -0
- package/dist/packages/persistence/migrations/meta/0001_snapshot.json +812 -0
- package/dist/packages/persistence/migrations/meta/0002_snapshot.json +859 -0
- package/dist/packages/persistence/migrations/meta/_journal.json +27 -0
- package/dist/packages/persistence/outbox/committer.js +54 -0
- package/dist/packages/persistence/outbox/store.js +66 -0
- package/dist/packages/persistence/repositories/configurations.js +41 -0
- package/dist/packages/persistence/repositories/delete-configuration.js +20 -0
- package/dist/packages/persistence/repositories/investigation-writes.js +72 -0
- package/dist/packages/persistence/repositories/investigations.js +27 -0
- package/dist/packages/persistence/repositories/observations.js +36 -0
- package/dist/packages/persistence/repositories/simulation-writes.js +39 -0
- package/dist/packages/persistence/repositories/simulations.js +23 -0
- package/dist/packages/persistence/schema/configurations.js +10 -0
- package/dist/packages/persistence/schema/index.js +5 -0
- package/dist/packages/persistence/schema/investigations.js +53 -0
- package/dist/packages/persistence/schema/observations.js +17 -0
- package/dist/packages/persistence/schema/outbox.js +26 -0
- package/dist/packages/persistence/schema/simulations.js +29 -0
- package/dist/packages/projects/index.js +2 -0
- package/dist/packages/projects/inspect.js +45 -0
- package/dist/packages/projects/suggest-target.js +34 -0
- package/dist/packages/projects/target-probe.js +61 -0
- package/dist/packages/runtime/deadline.js +11 -0
- package/dist/packages/runtime/evidence-store.js +76 -0
- package/dist/packages/runtime/execution-coordinator.js +223 -0
- package/dist/packages/runtime/execution-settlement.js +50 -0
- package/dist/packages/runtime/execution-types.js +1 -0
- package/dist/packages/runtime/host-config.js +26 -0
- package/dist/packages/runtime/host-storage.js +46 -0
- package/dist/packages/runtime/host.js +2 -0
- package/dist/packages/runtime/index.js +1 -0
- package/dist/packages/runtime/local-host.js +280 -0
- package/dist/packages/runtime/outbox-dispatcher.js +61 -0
- package/dist/packages/runtime/session-registry.js +42 -0
- package/dist/packages/server/configuration-routes.js +85 -0
- package/dist/packages/server/contracts.js +13 -0
- package/dist/packages/server/index.js +2 -0
- package/dist/packages/server/security.js +38 -0
- package/dist/packages/server/server.js +44 -0
- package/dist/packages/server/studio-assets.js +44 -0
- package/dist/packages/server/studio-session.js +28 -0
- package/dist/scripts/server/access-file.js +21 -0
- package/dist/scripts/server/cli-results.js +126 -0
- package/dist/scripts/server/open-browser.js +13 -0
- package/dist/scripts/server/run.js +76 -0
- package/dist/scripts/server/startup-error.js +17 -0
- package/dist/scripts/server/studio-entry-file.js +22 -0
- package/dist/studio/assets/index-DMnHlGCr.js +9 -0
- package/dist/studio/assets/index-Dh5KHl3J.css +1 -0
- package/dist/studio/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
- package/dist/studio/brand/logo-perspectives.svg +1 -0
- package/dist/studio/favicon.svg +1 -0
- package/dist/studio/index.html +14 -0
- package/dist/studio/licenses/jetbrains-mono.txt +93 -0
- package/dist/studio/licenses/space-grotesk.txt +93 -0
- package/dist/studio/participants/badger.png +0 -0
- package/dist/studio/participants/compass.png +0 -0
- package/dist/studio/participants/fox.png +0 -0
- package/dist/studio/participants/owl.png +0 -0
- package/dist/studio/participants/raven.png +0 -0
- package/dist/studio/participants/sparrow.png +0 -0
- package/offscript.mjs +178 -0
- package/package.json +100 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "6",
|
|
8
|
+
"when": 1793384398605,
|
|
9
|
+
"tag": "0000_initial_domain_storage",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "6",
|
|
15
|
+
"when": 1793386421172,
|
|
16
|
+
"tag": "0001_transactional_outbox",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "6",
|
|
22
|
+
"when": 1793402853622,
|
|
23
|
+
"tag": "0002_simulation_configuration",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { parseInvestigationEvent, serializeSemanticEvent } from "../../contracts/index.js";
|
|
3
|
+
import { outbox as eventOutbox } from "../schema/index.js";
|
|
4
|
+
import { readInvestigation } from "../codecs/investigation.js";
|
|
5
|
+
import { writeRequest, writeAssignment, writeCompletion } from "../repositories/investigation-writes.js";
|
|
6
|
+
export function investigationCommitter(db) {
|
|
7
|
+
function commit(input, expected) {
|
|
8
|
+
if (expected !== undefined && (!Number.isSafeInteger(expected) || expected < 0 || expected >= Number.MAX_SAFE_INTEGER))
|
|
9
|
+
throw new Error("Invalid revision.");
|
|
10
|
+
const event = parseInvestigationEvent(input);
|
|
11
|
+
const eventJson = serializeSemanticEvent(event);
|
|
12
|
+
const revision = expected === undefined ? 0 : expected + 1;
|
|
13
|
+
return db.transaction(tx => {
|
|
14
|
+
const previous = tx.select().from(eventOutbox).where(eq(eventOutbox.eventId, event.eventId)).get();
|
|
15
|
+
if (previous)
|
|
16
|
+
return previous.eventJson === eventJson && previous.revision === revision ? "duplicate" : "event-conflict";
|
|
17
|
+
let result;
|
|
18
|
+
if (event.type === "investigation.requested" && expected === undefined)
|
|
19
|
+
result = writeRequest(tx, event.payload.investigation);
|
|
20
|
+
else if (event.type === "investigation.assigned" && expected !== undefined)
|
|
21
|
+
result = writeAssignment(tx, event.payload.investigation, expected);
|
|
22
|
+
else if (event.type === "investigation.completed" && expected !== undefined)
|
|
23
|
+
result = writeCompletion(tx, event.payload.investigation, expected);
|
|
24
|
+
else
|
|
25
|
+
throw new Error("Invalid investigation commit type.");
|
|
26
|
+
if (result !== "created" && result !== "saved")
|
|
27
|
+
return result;
|
|
28
|
+
tx.insert(eventOutbox).values({ eventId: event.eventId, eventJson, simulationId: event.simulationId,
|
|
29
|
+
investigationId: event.payload.investigation.id, revision }).run();
|
|
30
|
+
return result;
|
|
31
|
+
}, { behavior: "immediate" });
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
async findInvestigation(id) { return db.transaction(tx => readInvestigation(tx, id)); },
|
|
35
|
+
async commitRequest(event) {
|
|
36
|
+
const result = commit(event);
|
|
37
|
+
if (result === "saved" || result === "not-found" || result === "conflict")
|
|
38
|
+
throw new Error("Invalid request commit result.");
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
async commitAssignment(event, revision) {
|
|
42
|
+
const result = commit(event, revision);
|
|
43
|
+
if (result === "created" || result === "already-exists")
|
|
44
|
+
throw new Error("Invalid assignment commit result.");
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
async commitCompletion(event, revision) {
|
|
48
|
+
const result = commit(event, revision);
|
|
49
|
+
if (result === "created" || result === "already-exists")
|
|
50
|
+
throw new Error("Invalid completion commit result.");
|
|
51
|
+
return result;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { and, eq, gt, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { validateClaim } from "../../application/index.js";
|
|
4
|
+
import { outbox } from "../schema/index.js";
|
|
5
|
+
export function outboxStore(db) {
|
|
6
|
+
function fence(id, token, now) {
|
|
7
|
+
if (!Number.isSafeInteger(now) || now < 0)
|
|
8
|
+
throw new Error("Invalid outbox time.");
|
|
9
|
+
return and(eq(outbox.eventId, id), eq(outbox.status, "leased"), eq(outbox.leaseToken, token), gt(outbox.leaseUntil, now));
|
|
10
|
+
}
|
|
11
|
+
function availability(now, at) {
|
|
12
|
+
if (!Number.isSafeInteger(at) || at < now)
|
|
13
|
+
throw new Error("Invalid retry availability.");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
async claim(options) {
|
|
17
|
+
validateClaim(options);
|
|
18
|
+
return db.transaction(tx => {
|
|
19
|
+
const candidates = tx.select().from(outbox).where(sql `
|
|
20
|
+
((${outbox.status} = 'pending' and ${outbox.availableAt} <= ${options.now})
|
|
21
|
+
or (${outbox.status} = 'leased' and ${outbox.leaseUntil} <= ${options.now}))
|
|
22
|
+
and not exists (select 1 from event_outbox older
|
|
23
|
+
where older.investigation_id = ${outbox.investigationId}
|
|
24
|
+
and older.sequence < ${outbox.sequence} and older.status != 'accepted')
|
|
25
|
+
`).orderBy(outbox.sequence).limit(options.limit).all();
|
|
26
|
+
const leases = [];
|
|
27
|
+
for (const row of candidates) {
|
|
28
|
+
if (row.attempts >= options.maxAttempts) {
|
|
29
|
+
tx.update(outbox).set({ status: "failed", leaseToken: null, leaseUntil: null, lastError: "ATTEMPTS_EXHAUSTED" })
|
|
30
|
+
.where(eq(outbox.sequence, row.sequence)).run();
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const lease = { ...row, status: "leased", attempts: row.attempts + 1,
|
|
34
|
+
leaseToken: `${options.token}:${randomUUID()}`, leaseUntil: options.now + options.leaseMs };
|
|
35
|
+
tx.update(outbox).set({ status: lease.status, attempts: lease.attempts,
|
|
36
|
+
leaseToken: lease.leaseToken, leaseUntil: lease.leaseUntil }).where(eq(outbox.sequence, row.sequence)).run();
|
|
37
|
+
leases.push(Object.freeze(lease));
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze(leases);
|
|
40
|
+
}, { behavior: "immediate" });
|
|
41
|
+
},
|
|
42
|
+
async accept(id, token, now) {
|
|
43
|
+
return db.update(outbox).set({ status: "accepted", acceptedAt: now, leaseToken: null, leaseUntil: null, lastError: null })
|
|
44
|
+
.where(fence(id, token, now)).run().changes === 1;
|
|
45
|
+
},
|
|
46
|
+
async defer(id, token, now, at) {
|
|
47
|
+
availability(now, at);
|
|
48
|
+
return db.update(outbox).set({ status: "pending", availableAt: at, leaseToken: null, leaseUntil: null,
|
|
49
|
+
attempts: sql `${outbox.attempts} - 1` }).where(fence(id, token, now)).run().changes === 1;
|
|
50
|
+
},
|
|
51
|
+
async reject(id, token, now, error, retryAt) {
|
|
52
|
+
if (retryAt !== null)
|
|
53
|
+
availability(now, retryAt);
|
|
54
|
+
return db.update(outbox).set({ status: retryAt === null ? "failed" : "pending", availableAt: retryAt ?? now,
|
|
55
|
+
leaseToken: null, leaseUntil: null, lastError: error }).where(fence(id, token, now)).run().changes === 1;
|
|
56
|
+
},
|
|
57
|
+
async inspect(id) {
|
|
58
|
+
const row = db.select().from(outbox).where(eq(outbox.eventId, id)).get();
|
|
59
|
+
return row && Object.freeze(row);
|
|
60
|
+
},
|
|
61
|
+
async hasPending(id) {
|
|
62
|
+
return !!db.select({ sequence: outbox.sequence }).from(outbox)
|
|
63
|
+
.where(and(eq(outbox.simulationId, id), inArray(outbox.status, ["pending", "leased"]))).limit(1).get();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { eq, sql } from "drizzle-orm";
|
|
2
|
+
import { configuredSimulation } from "../../application/index.js";
|
|
3
|
+
import { parseSimulationConfiguration } from "../../contracts/index.js";
|
|
4
|
+
import { simulationConfigurations } from "../schema/index.js";
|
|
5
|
+
import { PersistenceError } from "../errors.js";
|
|
6
|
+
import { json, stored } from "../codecs/values.js";
|
|
7
|
+
import { writeSimulation } from "./simulation-writes.js";
|
|
8
|
+
import { deleteConfiguration } from "./delete-configuration.js";
|
|
9
|
+
export function configurationRepository(db) {
|
|
10
|
+
return {
|
|
11
|
+
async deleteConfiguredSimulation(id) { return deleteConfiguration(db, id); },
|
|
12
|
+
async listConfigurations() {
|
|
13
|
+
const rows = db.select().from(simulationConfigurations).orderBy(sql `rowid desc`).limit(50).all();
|
|
14
|
+
return stored(() => Object.freeze(rows.map(row => parseSimulationConfiguration(json(row.configuration)))));
|
|
15
|
+
},
|
|
16
|
+
async createConfiguredSimulation(input, simulation) {
|
|
17
|
+
const configuration = parseSimulationConfiguration(input);
|
|
18
|
+
if (JSON.stringify(configuredSimulation(configuration)) !== JSON.stringify(simulation)) {
|
|
19
|
+
throw new PersistenceError("INVALID_WRITE", "Configuration must match a created simulation with ready users.");
|
|
20
|
+
}
|
|
21
|
+
return db.transaction(tx => {
|
|
22
|
+
const created = writeSimulation(tx, simulation);
|
|
23
|
+
if (created !== "created")
|
|
24
|
+
return created;
|
|
25
|
+
tx.insert(simulationConfigurations).values({ simulationId: configuration.simulationId, configuration: JSON.stringify(configuration) }).run();
|
|
26
|
+
return "created";
|
|
27
|
+
}, { behavior: "immediate" });
|
|
28
|
+
},
|
|
29
|
+
async findConfiguration(id) {
|
|
30
|
+
const row = db.select().from(simulationConfigurations).where(eq(simulationConfigurations.simulationId, id)).get();
|
|
31
|
+
if (!row)
|
|
32
|
+
return undefined;
|
|
33
|
+
return stored(() => {
|
|
34
|
+
const configuration = parseSimulationConfiguration(json(row.configuration));
|
|
35
|
+
if (configuration.simulationId !== id)
|
|
36
|
+
throw new PersistenceError("INVALID_STORED_DATA", "Configuration identity does not match its row.");
|
|
37
|
+
return configuration;
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { agents, simulations, simulationConfigurations, observations, investigations, outbox } from "../schema/index.js";
|
|
3
|
+
export function deleteConfiguration(db, id) {
|
|
4
|
+
return db.transaction(tx => {
|
|
5
|
+
if (!tx.select({ id: simulationConfigurations.simulationId }).from(simulationConfigurations).where(eq(simulationConfigurations.simulationId, id)).get())
|
|
6
|
+
return "not-found";
|
|
7
|
+
const simulation = tx.select().from(simulations).where(eq(simulations.id, id)).get();
|
|
8
|
+
const members = tx.select({ status: agents.status }).from(agents).where(eq(agents.simulationId, id)).all();
|
|
9
|
+
if (simulation?.status !== "created" || members.some(member => member.status !== "ready"))
|
|
10
|
+
return "not-allowed";
|
|
11
|
+
if (tx.select({ id: observations.id }).from(observations).where(eq(observations.simulationId, id)).limit(1).get() ||
|
|
12
|
+
tx.select({ id: investigations.id }).from(investigations).where(eq(investigations.simulationId, id)).limit(1).get() ||
|
|
13
|
+
tx.select({ id: outbox.eventId }).from(outbox).where(eq(outbox.simulationId, id)).limit(1).get())
|
|
14
|
+
return "not-allowed";
|
|
15
|
+
tx.delete(simulationConfigurations).where(eq(simulationConfigurations.simulationId, id)).run();
|
|
16
|
+
tx.delete(agents).where(eq(agents.simulationId, id)).run();
|
|
17
|
+
tx.delete(simulations).where(eq(simulations.id, id)).run();
|
|
18
|
+
return "deleted";
|
|
19
|
+
}, { behavior: "immediate" });
|
|
20
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { assignInvestigation, completeInvestigation } from "../../domain/index.js";
|
|
3
|
+
import { parseInvestigationSummary } from "../../contracts/index.js";
|
|
4
|
+
import { investigations, investigationAgents, investigationObservations } from "../schema/index.js";
|
|
5
|
+
import { readInvestigation } from "../codecs/investigation.js";
|
|
6
|
+
import { PersistenceError } from "../errors.js";
|
|
7
|
+
export function writeRequest(tx, input) {
|
|
8
|
+
const value = parseInvestigationSummary(input);
|
|
9
|
+
if (value.lifecycle.status !== "requested")
|
|
10
|
+
throw new PersistenceError("INVALID_WRITE", "Expected requested investigation.");
|
|
11
|
+
const result = tx.insert(investigations).values({
|
|
12
|
+
id: value.id, simulationId: value.simulationId, requestedBy: value.requestedBy,
|
|
13
|
+
hypothesis: value.hypothesis, requestedAt: value.requestedAt, status: "requested", revision: 0,
|
|
14
|
+
}).onConflictDoNothing({ target: investigations.id }).run();
|
|
15
|
+
if (!result.changes)
|
|
16
|
+
return "already-exists";
|
|
17
|
+
tx.insert(investigationObservations).values(value.observationIds.map((id, position) => ({
|
|
18
|
+
investigationId: value.id, simulationId: value.simulationId, observationId: id, position,
|
|
19
|
+
}))).run();
|
|
20
|
+
return "created";
|
|
21
|
+
}
|
|
22
|
+
export function writeAssignment(tx, input, expectedRevision) {
|
|
23
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0 || expectedRevision === Number.MAX_SAFE_INTEGER)
|
|
24
|
+
throw new PersistenceError("INVALID_WRITE", "Invalid revision.");
|
|
25
|
+
const value = parseInvestigationSummary(input);
|
|
26
|
+
if (value.lifecycle.status !== "assigned")
|
|
27
|
+
throw new PersistenceError("INVALID_WRITE", "Expected assigned investigation.");
|
|
28
|
+
const lifecycle = value.lifecycle;
|
|
29
|
+
const current = readInvestigation(tx, value.id);
|
|
30
|
+
if (!current)
|
|
31
|
+
return "not-found";
|
|
32
|
+
if (current.revision !== expectedRevision)
|
|
33
|
+
return "conflict";
|
|
34
|
+
const existing = current.investigation.lifecycle.status === "requested" ? [] : current.investigation.lifecycle.agentIds;
|
|
35
|
+
const expected = assignInvestigation(current.investigation, lifecycle.agentIds.filter((id) => !existing.includes(id)), lifecycle.assignedAt);
|
|
36
|
+
if (JSON.stringify(parseInvestigationSummary(expected)) !== JSON.stringify(value)) {
|
|
37
|
+
throw new PersistenceError("INVALID_WRITE", "Assignment must preserve the investigation and existing collaborators.");
|
|
38
|
+
}
|
|
39
|
+
const result = tx.update(investigations).set({ status: "assigned", assignedAt: lifecycle.assignedAt, revision: expectedRevision + 1 })
|
|
40
|
+
.where(and(eq(investigations.id, value.id), eq(investigations.revision, expectedRevision))).run();
|
|
41
|
+
if (!result.changes)
|
|
42
|
+
return "conflict";
|
|
43
|
+
tx.delete(investigationAgents).where(eq(investigationAgents.investigationId, value.id)).run();
|
|
44
|
+
tx.insert(investigationAgents).values(lifecycle.agentIds.map((id, position) => ({
|
|
45
|
+
investigationId: value.id, simulationId: value.simulationId, agentId: id, position,
|
|
46
|
+
}))).run();
|
|
47
|
+
return "saved";
|
|
48
|
+
}
|
|
49
|
+
export function writeCompletion(tx, input, expectedRevision) {
|
|
50
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0 || expectedRevision === Number.MAX_SAFE_INTEGER)
|
|
51
|
+
throw new PersistenceError("INVALID_WRITE", "Invalid revision.");
|
|
52
|
+
const value = parseInvestigationSummary(input);
|
|
53
|
+
if (value.lifecycle.status !== "completed")
|
|
54
|
+
throw new PersistenceError("INVALID_WRITE", "Expected completed investigation.");
|
|
55
|
+
const lifecycle = value.lifecycle;
|
|
56
|
+
const current = readInvestigation(tx, value.id);
|
|
57
|
+
if (!current)
|
|
58
|
+
return "not-found";
|
|
59
|
+
if (current.revision !== expectedRevision)
|
|
60
|
+
return "conflict";
|
|
61
|
+
const expected = completeInvestigation(current.investigation, lifecycle.result, lifecycle.completedBy, lifecycle.completedAt);
|
|
62
|
+
if (JSON.stringify(parseInvestigationSummary(expected)) !== JSON.stringify(value)) {
|
|
63
|
+
throw new PersistenceError("INVALID_WRITE", "Completion must match domain transition from assigned state.");
|
|
64
|
+
}
|
|
65
|
+
const result = tx.update(investigations).set({
|
|
66
|
+
status: "completed", completedAt: lifecycle.completedAt, completedBy: lifecycle.completedBy,
|
|
67
|
+
result: JSON.stringify(lifecycle.result), revision: expectedRevision + 1,
|
|
68
|
+
}).where(and(eq(investigations.id, value.id), eq(investigations.revision, expectedRevision))).run();
|
|
69
|
+
if (!result.changes)
|
|
70
|
+
return "conflict";
|
|
71
|
+
return "saved";
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { asc, eq } from "drizzle-orm";
|
|
2
|
+
import { readInvestigation } from "../codecs/investigation.js";
|
|
3
|
+
import { writeRequest, writeAssignment } from "./investigation-writes.js";
|
|
4
|
+
import { investigations } from "../schema/index.js";
|
|
5
|
+
import { PersistenceError } from "../errors.js";
|
|
6
|
+
export function investigationRepository(db) {
|
|
7
|
+
return {
|
|
8
|
+
async createInvestigation(input) { return db.transaction(tx => writeRequest(tx, input), { behavior: "immediate" }); },
|
|
9
|
+
async findInvestigation(id) { return db.transaction(tx => readInvestigation(tx, id)); },
|
|
10
|
+
async saveAssignment(input, revision) { return db.transaction(tx => writeAssignment(tx, input, revision), { behavior: "immediate" }); },
|
|
11
|
+
async listInvestigations(simulationId) {
|
|
12
|
+
return db.transaction(tx => {
|
|
13
|
+
const rows = tx.select({ id: investigations.id })
|
|
14
|
+
.from(investigations)
|
|
15
|
+
.where(eq(investigations.simulationId, simulationId))
|
|
16
|
+
.orderBy(asc(investigations.requestedAt))
|
|
17
|
+
.all();
|
|
18
|
+
return Object.freeze(rows.map(row => {
|
|
19
|
+
const loaded = readInvestigation(tx, row.id);
|
|
20
|
+
if (!loaded)
|
|
21
|
+
throw new PersistenceError("INVALID_STORED_DATA", "Investigation missing during list.");
|
|
22
|
+
return loaded.investigation;
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { asc, eq } from "drizzle-orm";
|
|
2
|
+
import { parseObservationSummary } from "../../contracts/index.js";
|
|
3
|
+
import { observations } from "../schema/index.js";
|
|
4
|
+
import { json, stored } from "../codecs/values.js";
|
|
5
|
+
export function observationRepository(db) {
|
|
6
|
+
return {
|
|
7
|
+
async createObservation(input) {
|
|
8
|
+
const observation = parseObservationSummary(input);
|
|
9
|
+
const result = db.insert(observations).values({
|
|
10
|
+
id: observation.id, simulationId: observation.simulationId, agentId: observation.agentId,
|
|
11
|
+
observedAt: observation.observedAt, description: observation.description, location: observation.context.location,
|
|
12
|
+
evidenceIds: JSON.stringify(observation.evidenceIds),
|
|
13
|
+
}).onConflictDoNothing({ target: observations.id }).run();
|
|
14
|
+
return result.changes ? "created" : "already-exists";
|
|
15
|
+
},
|
|
16
|
+
async findObservation(id) {
|
|
17
|
+
const row = db.select().from(observations).where(eq(observations.id, id)).get();
|
|
18
|
+
if (!row)
|
|
19
|
+
return undefined;
|
|
20
|
+
return stored(() => parseObservationSummary({
|
|
21
|
+
id: row.id, simulationId: row.simulationId, agentId: row.agentId, observedAt: row.observedAt,
|
|
22
|
+
description: row.description, context: { location: row.location }, evidenceIds: json(row.evidenceIds),
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
async listObservations(simulationId) {
|
|
26
|
+
const rows = db.select().from(observations)
|
|
27
|
+
.where(eq(observations.simulationId, simulationId))
|
|
28
|
+
.orderBy(asc(observations.observedAt))
|
|
29
|
+
.all();
|
|
30
|
+
return Object.freeze(rows.map(row => stored(() => parseObservationSummary({
|
|
31
|
+
id: row.id, simulationId: row.simulationId, agentId: row.agentId, observedAt: row.observedAt,
|
|
32
|
+
description: row.description, context: { location: row.location }, evidenceIds: json(row.evidenceIds),
|
|
33
|
+
}))));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { agents, simulations } from "../schema/index.js";
|
|
3
|
+
import { simulationRows } from "../codecs/simulation.js";
|
|
4
|
+
export function writeSimulation(tx, simulation) {
|
|
5
|
+
const { row, members } = simulationRows(simulation);
|
|
6
|
+
const created = tx.insert(simulations).values(row).onConflictDoNothing({ target: simulations.id }).run();
|
|
7
|
+
if (!created.changes)
|
|
8
|
+
return "already-exists";
|
|
9
|
+
if (members.length)
|
|
10
|
+
tx.insert(agents).values(members).run();
|
|
11
|
+
return "created";
|
|
12
|
+
}
|
|
13
|
+
export function updateSimulationAggregate(tx, simulation, expected) {
|
|
14
|
+
const current = tx.select().from(simulations).where(eq(simulations.id, simulation.id)).get();
|
|
15
|
+
if (!current)
|
|
16
|
+
return "not-found";
|
|
17
|
+
if (current.status !== expected.status)
|
|
18
|
+
return "conflict";
|
|
19
|
+
const members = tx.select().from(agents).where(eq(agents.simulationId, simulation.id)).all();
|
|
20
|
+
if (expected.agentStatuses) {
|
|
21
|
+
for (const [agentId, expectedStatus] of expected.agentStatuses) {
|
|
22
|
+
const member = members.find((entry) => entry.id === agentId);
|
|
23
|
+
if (!member || member.status !== expectedStatus)
|
|
24
|
+
return "conflict";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const { row, members: nextMembers } = simulationRows(simulation);
|
|
28
|
+
tx.update(simulations)
|
|
29
|
+
.set({ status: row.status, reason: row.reason })
|
|
30
|
+
.where(eq(simulations.id, simulation.id))
|
|
31
|
+
.run();
|
|
32
|
+
for (const member of nextMembers) {
|
|
33
|
+
tx.update(agents)
|
|
34
|
+
.set({ status: member.status, reason: member.reason })
|
|
35
|
+
.where(and(eq(agents.simulationId, simulation.id), eq(agents.id, member.id)))
|
|
36
|
+
.run();
|
|
37
|
+
}
|
|
38
|
+
return "updated";
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { asc, eq } from "drizzle-orm";
|
|
2
|
+
import { agents, simulations } from "../schema/index.js";
|
|
3
|
+
import { restoreSimulation } from "../codecs/simulation.js";
|
|
4
|
+
import { writeSimulation, updateSimulationAggregate } from "./simulation-writes.js";
|
|
5
|
+
export function simulationRepository(db) {
|
|
6
|
+
return {
|
|
7
|
+
async createSimulation(simulation) {
|
|
8
|
+
return db.transaction(tx => writeSimulation(tx, simulation), { behavior: "immediate" });
|
|
9
|
+
},
|
|
10
|
+
async updateSimulationAggregate(simulation, expected) {
|
|
11
|
+
return db.transaction(tx => updateSimulationAggregate(tx, simulation, expected), { behavior: "immediate" });
|
|
12
|
+
},
|
|
13
|
+
async findSimulation(id) {
|
|
14
|
+
return db.transaction((tx) => {
|
|
15
|
+
const row = tx.select().from(simulations).where(eq(simulations.id, id)).get();
|
|
16
|
+
if (!row)
|
|
17
|
+
return undefined;
|
|
18
|
+
const members = tx.select().from(agents).where(eq(agents.simulationId, id)).orderBy(asc(agents.position)).all();
|
|
19
|
+
return restoreSimulation(row, members);
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { check, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { simulations } from "./simulations.js";
|
|
4
|
+
export const simulationConfigurations = sqliteTable("simulation_configurations", {
|
|
5
|
+
simulationId: text("simulation_id").primaryKey().references(() => simulations.id),
|
|
6
|
+
configuration: text("configuration").notNull(),
|
|
7
|
+
}, table => [
|
|
8
|
+
check("configuration_json", sql `json_valid(${table.configuration}) and json_type(${table.configuration}) = 'object'`),
|
|
9
|
+
check("configuration_identity", sql `json_type(${table.configuration}, '$.simulationId') is 'text' and json_extract(${table.configuration}, '$.simulationId') = ${table.simulationId}`),
|
|
10
|
+
]);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { simulations, agents } from "./simulations.js";
|
|
2
|
+
export { observations } from "./observations.js";
|
|
3
|
+
export { investigations, investigationObservations, investigationAgents } from "./investigations.js";
|
|
4
|
+
export { outbox } from "./outbox.js";
|
|
5
|
+
export { simulationConfigurations } from "./configurations.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { check, foreignKey, index, integer, primaryKey, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { agents, simulations } from "./simulations.js";
|
|
4
|
+
import { observations } from "./observations.js";
|
|
5
|
+
export const investigations = sqliteTable("investigations", {
|
|
6
|
+
id: text("id").primaryKey(),
|
|
7
|
+
simulationId: text("simulation_id").notNull().references(() => simulations.id),
|
|
8
|
+
requestedBy: text("requested_by").notNull(),
|
|
9
|
+
hypothesis: text("hypothesis").notNull(),
|
|
10
|
+
requestedAt: text("requested_at").notNull(),
|
|
11
|
+
status: text("status").notNull(),
|
|
12
|
+
assignedAt: text("assigned_at"),
|
|
13
|
+
completedAt: text("completed_at"),
|
|
14
|
+
completedBy: text("completed_by"),
|
|
15
|
+
result: text("result"),
|
|
16
|
+
revision: integer("revision").notNull().default(0),
|
|
17
|
+
}, (table) => [
|
|
18
|
+
unique("investigation_scope").on(table.id, table.simulationId),
|
|
19
|
+
foreignKey({ columns: [table.simulationId, table.requestedBy], foreignColumns: [agents.simulationId, agents.id] }),
|
|
20
|
+
foreignKey({ columns: [table.simulationId, table.completedBy], foreignColumns: [agents.simulationId, agents.id] }),
|
|
21
|
+
index("investigation_simulation_status").on(table.simulationId, table.status, table.requestedAt, table.id),
|
|
22
|
+
check("investigation_revision", sql `${table.revision} >= 0 and ${table.revision} <= 9007199254740991`),
|
|
23
|
+
check("investigation_state", sql `(
|
|
24
|
+
(${table.status} = 'requested' and ${table.assignedAt} is null and ${table.completedAt} is null and ${table.completedBy} is null and ${table.result} is null)
|
|
25
|
+
or (${table.status} = 'assigned' and ${table.assignedAt} is not null and ${table.completedAt} is null and ${table.completedBy} is null and ${table.result} is null)
|
|
26
|
+
or (${table.status} = 'completed' and ${table.assignedAt} is not null and ${table.completedAt} is not null and ${table.completedBy} is not null and ${table.result} is not null)
|
|
27
|
+
)`),
|
|
28
|
+
check("investigation_result_json", sql `${table.result} is null or json_valid(${table.result})`),
|
|
29
|
+
]);
|
|
30
|
+
export const investigationObservations = sqliteTable("investigation_observations", {
|
|
31
|
+
investigationId: text("investigation_id").notNull(),
|
|
32
|
+
simulationId: text("simulation_id").notNull(),
|
|
33
|
+
observationId: text("observation_id").notNull(),
|
|
34
|
+
position: integer("position").notNull(),
|
|
35
|
+
}, (table) => [
|
|
36
|
+
primaryKey({ columns: [table.investigationId, table.observationId] }),
|
|
37
|
+
unique("investigation_observation_position").on(table.investigationId, table.position),
|
|
38
|
+
foreignKey({ columns: [table.investigationId, table.simulationId], foreignColumns: [investigations.id, investigations.simulationId] }),
|
|
39
|
+
foreignKey({ columns: [table.observationId, table.simulationId], foreignColumns: [observations.id, observations.simulationId] }),
|
|
40
|
+
check("investigation_observation_nonnegative_position", sql `${table.position} >= 0`),
|
|
41
|
+
]);
|
|
42
|
+
export const investigationAgents = sqliteTable("investigation_agents", {
|
|
43
|
+
investigationId: text("investigation_id").notNull(),
|
|
44
|
+
simulationId: text("simulation_id").notNull(),
|
|
45
|
+
agentId: text("agent_id").notNull(),
|
|
46
|
+
position: integer("position").notNull(),
|
|
47
|
+
}, (table) => [
|
|
48
|
+
primaryKey({ columns: [table.investigationId, table.agentId] }),
|
|
49
|
+
unique("investigation_agent_position").on(table.investigationId, table.position),
|
|
50
|
+
foreignKey({ columns: [table.investigationId, table.simulationId], foreignColumns: [investigations.id, investigations.simulationId] }),
|
|
51
|
+
foreignKey({ columns: [table.simulationId, table.agentId], foreignColumns: [agents.simulationId, agents.id] }),
|
|
52
|
+
check("investigation_agent_nonnegative_position", sql `${table.position} >= 0`),
|
|
53
|
+
]);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { check, foreignKey, index, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { agents, simulations } from "./simulations.js";
|
|
4
|
+
export const observations = sqliteTable("observations", {
|
|
5
|
+
id: text("id").primaryKey(),
|
|
6
|
+
simulationId: text("simulation_id").notNull().references(() => simulations.id),
|
|
7
|
+
agentId: text("agent_id").notNull(),
|
|
8
|
+
observedAt: text("observed_at").notNull(),
|
|
9
|
+
description: text("description").notNull(),
|
|
10
|
+
location: text("location").notNull(),
|
|
11
|
+
evidenceIds: text("evidence_ids").notNull(),
|
|
12
|
+
}, (table) => [
|
|
13
|
+
unique("observation_scope").on(table.id, table.simulationId),
|
|
14
|
+
foreignKey({ columns: [table.simulationId, table.agentId], foreignColumns: [agents.simulationId, agents.id] }),
|
|
15
|
+
index("observation_simulation_time").on(table.simulationId, table.observedAt, table.id),
|
|
16
|
+
check("observation_evidence_json", sql `json_valid(${table.evidenceIds})`),
|
|
17
|
+
]);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { check, foreignKey, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { investigations } from "./investigations.js";
|
|
4
|
+
export const outbox = sqliteTable("event_outbox", {
|
|
5
|
+
sequence: integer("sequence").primaryKey({ autoIncrement: true }),
|
|
6
|
+
eventId: text("event_id").notNull().unique(),
|
|
7
|
+
simulationId: text("simulation_id").notNull(),
|
|
8
|
+
investigationId: text("investigation_id").notNull(),
|
|
9
|
+
revision: integer("revision").notNull(),
|
|
10
|
+
eventJson: text("event_json").notNull(),
|
|
11
|
+
status: text("status", { enum: ["pending", "leased", "accepted", "failed"] }).notNull().default("pending"),
|
|
12
|
+
attempts: integer("attempts").notNull().default(0),
|
|
13
|
+
availableAt: integer("available_at").notNull().default(0),
|
|
14
|
+
leaseToken: text("lease_token"), leaseUntil: integer("lease_until"), acceptedAt: integer("accepted_at"),
|
|
15
|
+
lastError: text("last_error", { enum: ["PUBLISH_REJECTED", "INVALID_EVENT", "ATTEMPTS_EXHAUSTED"] }),
|
|
16
|
+
}, (table) => [
|
|
17
|
+
foreignKey({ columns: [table.investigationId, table.simulationId], foreignColumns: [investigations.id, investigations.simulationId] }),
|
|
18
|
+
unique("outbox_investigation_revision").on(table.investigationId, table.revision),
|
|
19
|
+
index("outbox_ready").on(table.status, table.availableAt, table.sequence),
|
|
20
|
+
index("outbox_aggregate_order").on(table.investigationId, table.sequence, table.status),
|
|
21
|
+
check("outbox_json", sql `json_valid(${table.eventJson})`),
|
|
22
|
+
check("outbox_numbers", sql `${table.revision} >= 0 and ${table.attempts} >= 0 and ${table.availableAt} >= 0`),
|
|
23
|
+
check("outbox_status", sql `${table.status} in ('pending','leased','accepted','failed')`),
|
|
24
|
+
check("outbox_lease", sql `(${table.status} = 'leased' and ${table.leaseToken} is not null and ${table.leaseUntil} is not null) or (${table.status} != 'leased' and ${table.leaseToken} is null and ${table.leaseUntil} is null)`),
|
|
25
|
+
check("outbox_acceptance", sql `(${table.status} = 'accepted' and ${table.acceptedAt} is not null) or (${table.status} != 'accepted' and ${table.acceptedAt} is null)`),
|
|
26
|
+
]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { check, integer, primaryKey, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
3
|
+
export const simulations = sqliteTable("simulations", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
environment: text("environment").notNull(),
|
|
6
|
+
deployment: text("deployment").notNull(),
|
|
7
|
+
status: text("status").notNull(),
|
|
8
|
+
reason: text("reason"),
|
|
9
|
+
}, (table) => [
|
|
10
|
+
check("simulation_environment", sql `${table.environment} = 'web'`),
|
|
11
|
+
check("simulation_deployment", sql `${table.deployment} in ('local','development','test','staging','production')`),
|
|
12
|
+
check("simulation_status", sql `${table.status} in ('created','running','completed','failed','cancelled')`),
|
|
13
|
+
check("simulation_reason", sql `(${table.status} = 'failed' and ${table.reason} is not null and length(trim(${table.reason})) > 0) or (${table.status} != 'failed' and ${table.reason} is null)`),
|
|
14
|
+
]);
|
|
15
|
+
export const agents = sqliteTable("agents", {
|
|
16
|
+
simulationId: text("simulation_id").notNull().references(() => simulations.id),
|
|
17
|
+
id: text("id").notNull(),
|
|
18
|
+
position: integer("position").notNull(),
|
|
19
|
+
profile: text("profile").notNull(),
|
|
20
|
+
status: text("status").notNull(),
|
|
21
|
+
reason: text("reason"),
|
|
22
|
+
}, (table) => [
|
|
23
|
+
primaryKey({ columns: [table.simulationId, table.id] }),
|
|
24
|
+
unique("agent_position").on(table.simulationId, table.position),
|
|
25
|
+
check("agent_nonnegative_position", sql `${table.position} >= 0`),
|
|
26
|
+
check("agent_profile_json", sql `json_valid(${table.profile})`),
|
|
27
|
+
check("agent_status", sql `${table.status} in ('ready','running','completed','failed','cancelled')`),
|
|
28
|
+
check("agent_reason", sql `(${table.status} = 'failed' and ${table.reason} is not null and length(trim(${table.reason})) > 0) or (${table.status} != 'failed' and ${table.reason} is null)`),
|
|
29
|
+
]);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { open, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { suggestTarget } from "./suggest-target.js";
|
|
4
|
+
export class ProjectInspectionError extends Error {
|
|
5
|
+
constructor(message, options) { super(message, options); this.name = "ProjectInspectionError"; }
|
|
6
|
+
}
|
|
7
|
+
export async function inspectProject(projectRoot) {
|
|
8
|
+
try {
|
|
9
|
+
if (!projectRoot.trim())
|
|
10
|
+
throw new ProjectInspectionError("An explicit project path is required.");
|
|
11
|
+
const rootPath = await realpath(resolve(projectRoot));
|
|
12
|
+
if (!(await stat(rootPath)).isDirectory())
|
|
13
|
+
throw new ProjectInspectionError("The project path must be a directory.");
|
|
14
|
+
const fallback = Object.freeze({ rootPath, suggestedName: basename(rootPath) || "Project", nameSource: "directory" });
|
|
15
|
+
let file;
|
|
16
|
+
try {
|
|
17
|
+
file = await open(join(rootPath, "package.json"), "r");
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
21
|
+
return fallback;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const info = await file.stat();
|
|
26
|
+
if (!info.isFile() || info.size > 1_048_576)
|
|
27
|
+
throw new ProjectInspectionError("Project manifest must be a regular file of at most 1 MiB.");
|
|
28
|
+
const parsed = JSON.parse(await file.readFile({ encoding: "utf8" }));
|
|
29
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
30
|
+
throw new ProjectInspectionError("Project manifest must contain a JSON object.");
|
|
31
|
+
const name = "name" in parsed ? parsed.name : undefined;
|
|
32
|
+
const targetSuggestion = suggestTarget(parsed);
|
|
33
|
+
return Object.freeze({ ...(typeof name === "string" && name.trim() ? { rootPath, suggestedName: name.trim(), nameSource: "package" } : fallback),
|
|
34
|
+
...(targetSuggestion ? { targetSuggestion } : {}) });
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await file.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (cause) {
|
|
41
|
+
if (cause instanceof ProjectInspectionError)
|
|
42
|
+
throw cause;
|
|
43
|
+
throw new ProjectInspectionError("Could not read the selected project.", { cause });
|
|
44
|
+
}
|
|
45
|
+
}
|