@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.
- package/LICENSE +201 -0
- package/README.md +148 -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-Dh5KHl3J.css +1 -0
- package/dist/studio/assets/index-uXpu7a13.js +9 -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 +104 -0
- package/package.json +100 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { parseEventEnvelope } from "./event-envelope.js";
|
|
3
|
+
import { parseInvestigationSummary } from "./investigation-summary.js";
|
|
4
|
+
import { record } from "./validation.js";
|
|
5
|
+
export function parseInvestigationEvent(value) {
|
|
6
|
+
const { input, base } = parseEventEnvelope(value);
|
|
7
|
+
const payload = record(input.payload, "event.payload", ["investigation"]);
|
|
8
|
+
const investigation = parseInvestigationSummary(payload.investigation);
|
|
9
|
+
if (investigation.simulationId !== base.simulationId) {
|
|
10
|
+
throw new ContractValidationError("event.payload.investigation.simulationId", "Simulation ID does not match the envelope.");
|
|
11
|
+
}
|
|
12
|
+
const state = investigation.lifecycle;
|
|
13
|
+
switch (input.type) {
|
|
14
|
+
case "investigation.requested":
|
|
15
|
+
if (state.status !== "requested")
|
|
16
|
+
break;
|
|
17
|
+
if (base.origin.kind === "agent" && base.origin.agentId !== investigation.requestedBy) {
|
|
18
|
+
throw new ContractValidationError("event.origin.agentId", "Origin must match the requesting agent.");
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({ ...base, type: input.type, payload: Object.freeze({
|
|
21
|
+
investigation: Object.freeze({ ...investigation, lifecycle: state }),
|
|
22
|
+
}) });
|
|
23
|
+
case "investigation.assigned":
|
|
24
|
+
if (state.status !== "assigned")
|
|
25
|
+
break;
|
|
26
|
+
// The assigning origin need not be an assignee. Authorization is external.
|
|
27
|
+
return Object.freeze({ ...base, type: input.type, payload: Object.freeze({
|
|
28
|
+
investigation: Object.freeze({ ...investigation, lifecycle: state }),
|
|
29
|
+
}) });
|
|
30
|
+
case "investigation.completed":
|
|
31
|
+
if (state.status !== "completed")
|
|
32
|
+
break;
|
|
33
|
+
if (base.origin.kind === "agent" && base.origin.agentId !== state.completedBy) {
|
|
34
|
+
throw new ContractValidationError("event.origin.agentId", "Origin must match the result author.");
|
|
35
|
+
}
|
|
36
|
+
return Object.freeze({ ...base, type: input.type, payload: Object.freeze({
|
|
37
|
+
investigation: Object.freeze({ ...investigation, lifecycle: state }),
|
|
38
|
+
}) });
|
|
39
|
+
default:
|
|
40
|
+
throw new ContractValidationError("event.type", "Unknown investigation event type.");
|
|
41
|
+
}
|
|
42
|
+
throw new ContractValidationError("event.payload.investigation.lifecycle.status", "Lifecycle does not match event type.");
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createInvestigationResult, evidenceId, investigationId, observationId } from "../domain/index.js";
|
|
2
|
+
import { ContractValidationError } from "./errors.js";
|
|
3
|
+
import { oneOf, readAgentId, readSimulationId, record, text, timestamp } from "./validation.js";
|
|
4
|
+
function references(value, path, read, required) {
|
|
5
|
+
if (!Array.isArray(value))
|
|
6
|
+
throw new ContractValidationError(path, "Expected an array.");
|
|
7
|
+
const values = Array.from(value, (entry, index) => read(text(entry, `${path}[${index}]`)));
|
|
8
|
+
if (new Set(values).size !== values.length || (required && values.length === 0)) {
|
|
9
|
+
throw new ContractValidationError(path, "References must be unique and the required list must not be empty.");
|
|
10
|
+
}
|
|
11
|
+
return Object.freeze(values);
|
|
12
|
+
}
|
|
13
|
+
export function parseInvestigationResult(value) {
|
|
14
|
+
const path = "investigation.lifecycle.result";
|
|
15
|
+
const input = record(value, path, ["outcome", "explanation", "evidenceIds"]);
|
|
16
|
+
const outcome = oneOf(input.outcome, ["supported", "contradicted", "inconclusive"], `${path}.outcome`);
|
|
17
|
+
return createInvestigationResult({
|
|
18
|
+
outcome, explanation: text(input.explanation, `${path}.explanation`),
|
|
19
|
+
evidenceIds: references(input.evidenceIds, `${path}.evidenceIds`, evidenceId, outcome !== "inconclusive"),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function lifecycle(value, requestedAt) {
|
|
23
|
+
const path = "investigation.lifecycle";
|
|
24
|
+
const shape = record(value, path, ["status", "agentIds", "assignedAt", "completedAt", "completedBy", "result"]);
|
|
25
|
+
const status = oneOf(shape.status, ["requested", "assigned", "completed"], `${path}.status`);
|
|
26
|
+
if (status === "requested") {
|
|
27
|
+
record(value, path, ["status"]);
|
|
28
|
+
return Object.freeze({ status });
|
|
29
|
+
}
|
|
30
|
+
if (status === "assigned")
|
|
31
|
+
record(value, path, ["status", "agentIds", "assignedAt"]);
|
|
32
|
+
const agentIds = references(shape.agentIds, `${path}.agentIds`, (id) => readAgentId(id, `${path}.agentIds`), true);
|
|
33
|
+
const assignedAt = timestamp(shape.assignedAt, `${path}.assignedAt`);
|
|
34
|
+
if (assignedAt < requestedAt)
|
|
35
|
+
throw new ContractValidationError(`${path}.assignedAt`, "Assignment precedes request.");
|
|
36
|
+
if (status === "assigned")
|
|
37
|
+
return Object.freeze({ status, agentIds, assignedAt });
|
|
38
|
+
const completedBy = readAgentId(shape.completedBy, `${path}.completedBy`);
|
|
39
|
+
const completedAt = timestamp(shape.completedAt, `${path}.completedAt`);
|
|
40
|
+
if (!agentIds.includes(completedBy)) {
|
|
41
|
+
throw new ContractValidationError(`${path}.completedBy`, "Result author must be an assigned agent.");
|
|
42
|
+
}
|
|
43
|
+
if (completedAt < assignedAt)
|
|
44
|
+
throw new ContractValidationError(`${path}.completedAt`, "Completion precedes assignment.");
|
|
45
|
+
return Object.freeze({ status, agentIds, assignedAt, completedBy, completedAt, result: parseInvestigationResult(shape.result) });
|
|
46
|
+
}
|
|
47
|
+
export function parseInvestigationSummary(value) {
|
|
48
|
+
const input = record(value, "investigation", [
|
|
49
|
+
"id", "simulationId", "requestedBy", "hypothesis", "requestedAt", "observationIds", "lifecycle",
|
|
50
|
+
]);
|
|
51
|
+
const requestedAt = timestamp(input.requestedAt, "investigation.requestedAt");
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
id: investigationId(text(input.id, "investigation.id")),
|
|
54
|
+
simulationId: readSimulationId(input.simulationId, "investigation.simulationId"),
|
|
55
|
+
requestedBy: readAgentId(input.requestedBy, "investigation.requestedBy"),
|
|
56
|
+
hypothesis: text(input.hypothesis, "investigation.hypothesis"), requestedAt,
|
|
57
|
+
observationIds: references(input.observationIds, "investigation.observationIds", observationId, true),
|
|
58
|
+
lifecycle: lifecycle(input.lifecycle, requestedAt),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseAgentSummary, parseSimulationSummary } from "./summaries.js";
|
|
2
|
+
export function toAgentSummary(simulationId, agent) {
|
|
3
|
+
return parseAgentSummary({
|
|
4
|
+
id: agent.id,
|
|
5
|
+
simulationId,
|
|
6
|
+
name: agent.profile.name,
|
|
7
|
+
lifecycle: agent.lifecycle,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function toSimulationSummary(simulation) {
|
|
11
|
+
return parseSimulationSummary({
|
|
12
|
+
id: simulation.id,
|
|
13
|
+
environment: simulation.environment,
|
|
14
|
+
lifecycle: simulation.lifecycle,
|
|
15
|
+
agentIds: simulation.agents.map((agent) => agent.id),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createObservation, evidenceId, observationId } from "../domain/index.js";
|
|
2
|
+
import { ContractValidationError } from "./errors.js";
|
|
3
|
+
import { readAgentId, readSimulationId, record, text, timestamp } from "./validation.js";
|
|
4
|
+
export function parseObservationSummary(value) {
|
|
5
|
+
const input = record(value, "observation", [
|
|
6
|
+
"id", "simulationId", "agentId", "observedAt", "description", "context", "evidenceIds",
|
|
7
|
+
]);
|
|
8
|
+
const context = record(input.context, "observation.context", ["location"]);
|
|
9
|
+
if (!Array.isArray(input.evidenceIds)) {
|
|
10
|
+
throw new ContractValidationError("observation.evidenceIds", "Expected an array.");
|
|
11
|
+
}
|
|
12
|
+
const evidenceIds = Array.from(input.evidenceIds, (id, index) => evidenceId(text(id, `observation.evidenceIds[${index}]`)));
|
|
13
|
+
if (new Set(evidenceIds).size !== evidenceIds.length) {
|
|
14
|
+
throw new ContractValidationError("observation.evidenceIds", "Evidence references must be unique.");
|
|
15
|
+
}
|
|
16
|
+
return createObservation({
|
|
17
|
+
id: observationId(text(input.id, "observation.id")),
|
|
18
|
+
simulationId: readSimulationId(input.simulationId, "observation.simulationId"),
|
|
19
|
+
agentId: readAgentId(input.agentId, "observation.agentId"),
|
|
20
|
+
observedAt: timestamp(input.observedAt, "observation.observedAt"),
|
|
21
|
+
description: text(input.description, "observation.description"),
|
|
22
|
+
context: { location: text(context.location, "observation.context.location") },
|
|
23
|
+
evidenceIds,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { parseEventEnvelope } from "./event-envelope.js";
|
|
3
|
+
import { parseLifecycleEvent } from "./event-validation.js";
|
|
4
|
+
import { parseObservationSummary } from "./observation-summary.js";
|
|
5
|
+
import { record } from "./validation.js";
|
|
6
|
+
import { parseInvestigationEvent } from "./investigation-events.js";
|
|
7
|
+
export function parseSemanticEvent(value) {
|
|
8
|
+
const { input, base } = parseEventEnvelope(value);
|
|
9
|
+
if (input.type === "investigation.requested" || input.type === "investigation.assigned" || input.type === "investigation.completed") {
|
|
10
|
+
return parseInvestigationEvent(value);
|
|
11
|
+
}
|
|
12
|
+
if (input.type !== "observation.created")
|
|
13
|
+
return parseLifecycleEvent(value);
|
|
14
|
+
const payload = record(input.payload, "event.payload", ["observation"]);
|
|
15
|
+
const observation = parseObservationSummary(payload.observation);
|
|
16
|
+
if (observation.simulationId !== base.simulationId) {
|
|
17
|
+
throw new ContractValidationError("event.payload.observation.simulationId", "Simulation ID does not match the envelope.");
|
|
18
|
+
}
|
|
19
|
+
// A system may record an agent's observation. An agent-originated event must
|
|
20
|
+
// attribute it to that same agent; this checks consistency, not authenticity.
|
|
21
|
+
if (base.origin.kind === "agent" && base.origin.agentId !== observation.agentId) {
|
|
22
|
+
throw new ContractValidationError("event.payload.observation.agentId", "Author does not match the originating agent.");
|
|
23
|
+
}
|
|
24
|
+
return Object.freeze({ ...base, type: "observation.created", payload: Object.freeze({ observation }) });
|
|
25
|
+
}
|
|
26
|
+
export function createSemanticEvent(input) {
|
|
27
|
+
return parseSemanticEvent(input);
|
|
28
|
+
}
|
|
29
|
+
export function serializeSemanticEvent(event) {
|
|
30
|
+
return JSON.stringify(parseSemanticEvent(event));
|
|
31
|
+
}
|
|
32
|
+
export function deserializeSemanticEvent(json) {
|
|
33
|
+
let input;
|
|
34
|
+
try {
|
|
35
|
+
input = JSON.parse(json);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new ContractValidationError("event", "Invalid JSON.");
|
|
39
|
+
}
|
|
40
|
+
return parseSemanticEvent(input);
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { parseLifecycleEvent } from "./event-validation.js";
|
|
3
|
+
export function createLifecycleEvent(input) {
|
|
4
|
+
return parseLifecycleEvent(input);
|
|
5
|
+
}
|
|
6
|
+
export function serializeLifecycleEvent(event) {
|
|
7
|
+
return JSON.stringify(parseLifecycleEvent(event));
|
|
8
|
+
}
|
|
9
|
+
export function deserializeLifecycleEvent(json) {
|
|
10
|
+
let input;
|
|
11
|
+
try {
|
|
12
|
+
input = JSON.parse(json);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new ContractValidationError("event", "Invalid JSON.");
|
|
16
|
+
}
|
|
17
|
+
return parseLifecycleEvent(input);
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseExecutionLimits, parseSimulationSetup } from "./simulation-setup.js";
|
|
2
|
+
import { oneOf, readSimulationId, record, text } from "./validation.js";
|
|
3
|
+
import { parseTargetSuggestion } from "./target-connection.js";
|
|
4
|
+
export function parseProjectDescriptor(value) {
|
|
5
|
+
const input = record(value, "project", ["rootPath", "suggestedName", "nameSource", "targetSuggestion"]);
|
|
6
|
+
return Object.freeze({ rootPath: text(input.rootPath, "project.rootPath"), suggestedName: text(input.suggestedName, "project.suggestedName"),
|
|
7
|
+
nameSource: oneOf(input.nameSource, ["package", "directory"], "project.nameSource"),
|
|
8
|
+
...(input.targetSuggestion === undefined ? {} : { targetSuggestion: parseTargetSuggestion(input.targetSuggestion) }) });
|
|
9
|
+
}
|
|
10
|
+
export function parseSimulationConfiguration(value) {
|
|
11
|
+
const input = record(value, "configuration", ["simulationId", "projectRoot", "name", "targetUrl", "environment", "goal", "userProfileIds", "limits"]);
|
|
12
|
+
const setup = parseSimulationSetup({ name: text(input.name, "configuration.name"),
|
|
13
|
+
targetUrl: input.targetUrl, environment: input.environment, goal: input.goal,
|
|
14
|
+
userProfileIds: input.userProfileIds, limits: parseExecutionLimits(input.limits) });
|
|
15
|
+
return Object.freeze({ ...setup, simulationId: readSimulationId(input.simulationId, "configuration.simulationId"),
|
|
16
|
+
projectRoot: text(input.projectRoot, "configuration.projectRoot"), name: text(setup.name, "configuration.name") });
|
|
17
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { simulationId } from "../domain/index.js";
|
|
2
|
+
import { ContractValidationError } from "./errors.js";
|
|
3
|
+
import { oneOf, record, text } from "./validation.js";
|
|
4
|
+
export const simulationExecutionModes = ["controlled-demo", "browser-exploration"];
|
|
5
|
+
export const simulationExecutionPhases = [
|
|
6
|
+
"idle",
|
|
7
|
+
"starting",
|
|
8
|
+
"running",
|
|
9
|
+
"stopping",
|
|
10
|
+
"completed",
|
|
11
|
+
"failed",
|
|
12
|
+
"cancelled",
|
|
13
|
+
"interrupted",
|
|
14
|
+
];
|
|
15
|
+
export function parseStartSimulationRequest(value) {
|
|
16
|
+
const input = record(value, "startRequest", ["mode"]);
|
|
17
|
+
const mode = oneOf(input.mode, simulationExecutionModes, "startRequest.mode");
|
|
18
|
+
return Object.freeze({ mode });
|
|
19
|
+
}
|
|
20
|
+
export function parseSimulationExecutionStatus(value) {
|
|
21
|
+
const input = record(value, "executionStatus", [
|
|
22
|
+
"simulationId", "phase", "mode", "turnsCount", "activeAgents", "failureReason", "startedAt", "stoppedAt", "recentActivity",
|
|
23
|
+
]);
|
|
24
|
+
const id = simulationId(text(input.simulationId, "executionStatus.simulationId"));
|
|
25
|
+
const phase = oneOf(input.phase, simulationExecutionPhases, "executionStatus.phase");
|
|
26
|
+
const mode = input.mode !== undefined ? oneOf(input.mode, simulationExecutionModes, "executionStatus.mode") : undefined;
|
|
27
|
+
if (typeof input.turnsCount !== "number" || !Number.isSafeInteger(input.turnsCount) || input.turnsCount < 0) {
|
|
28
|
+
throw new ContractValidationError("executionStatus.turnsCount", "Expected a non-negative integer.");
|
|
29
|
+
}
|
|
30
|
+
if (!Array.isArray(input.activeAgents) || input.activeAgents.some((item) => typeof item !== "string")) {
|
|
31
|
+
throw new ContractValidationError("executionStatus.activeAgents", "Expected an array of agent identifiers.");
|
|
32
|
+
}
|
|
33
|
+
const failureReason = input.failureReason !== undefined ? text(input.failureReason, "executionStatus.failureReason") : undefined;
|
|
34
|
+
const startedAt = input.startedAt !== undefined ? text(input.startedAt, "executionStatus.startedAt") : undefined;
|
|
35
|
+
const stoppedAt = input.stoppedAt !== undefined ? text(input.stoppedAt, "executionStatus.stoppedAt") : undefined;
|
|
36
|
+
let recentActivity;
|
|
37
|
+
if (input.recentActivity !== undefined) {
|
|
38
|
+
if (!Array.isArray(input.recentActivity)) {
|
|
39
|
+
throw new ContractValidationError("executionStatus.recentActivity", "Expected an array.");
|
|
40
|
+
}
|
|
41
|
+
recentActivity = Object.freeze(input.recentActivity.map((item, i) => {
|
|
42
|
+
const act = record(item, `executionStatus.recentActivity[${i}]`, ["agentId", "action", "location", "timestamp"]);
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
agentId: text(act.agentId, `executionStatus.recentActivity[${i}].agentId`),
|
|
45
|
+
action: text(act.action, `executionStatus.recentActivity[${i}].action`),
|
|
46
|
+
timestamp: text(act.timestamp, `executionStatus.recentActivity[${i}].timestamp`),
|
|
47
|
+
...(act.location !== undefined ? { location: text(act.location, `executionStatus.recentActivity[${i}].location`) } : {}),
|
|
48
|
+
});
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
simulationId: id,
|
|
53
|
+
phase,
|
|
54
|
+
...(mode !== undefined ? { mode } : {}),
|
|
55
|
+
turnsCount: input.turnsCount,
|
|
56
|
+
activeAgents: Object.freeze([...input.activeAgents]),
|
|
57
|
+
...(failureReason ? { failureReason } : {}),
|
|
58
|
+
...(startedAt ? { startedAt } : {}),
|
|
59
|
+
...(stoppedAt ? { stoppedAt } : {}),
|
|
60
|
+
...(recentActivity ? { recentActivity } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parseObservationSummary } from "./observation-summary.js";
|
|
2
|
+
import { parseInvestigationSummary } from "./investigation-summary.js";
|
|
3
|
+
import { oneOf, readSimulationId, record, text } from "./validation.js";
|
|
4
|
+
import { ContractValidationError } from "./errors.js";
|
|
5
|
+
function parseFindingAssessment(value, path) {
|
|
6
|
+
const item = record(value, path, [
|
|
7
|
+
"investigationId", "hypothesis", "outcome", "confidence", "explanation", "isConfirmedFinding", "participatingAgents", "evidenceIds",
|
|
8
|
+
]);
|
|
9
|
+
const outcome = oneOf(item.outcome, ["supported", "contradicted", "inconclusive"], `${path}.outcome`);
|
|
10
|
+
const confidence = oneOf(item.confidence, ["provisional", "corroborated", "inconclusive", "refuted"], `${path}.confidence`);
|
|
11
|
+
if (typeof item.isConfirmedFinding !== "boolean") {
|
|
12
|
+
throw new ContractValidationError(`${path}.isConfirmedFinding`, "Expected a boolean.");
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(item.participatingAgents)) {
|
|
15
|
+
throw new ContractValidationError(`${path}.participatingAgents`, "Expected an array.");
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(item.evidenceIds)) {
|
|
18
|
+
throw new ContractValidationError(`${path}.evidenceIds`, "Expected an array.");
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
investigationId: text(item.investigationId, `${path}.investigationId`),
|
|
22
|
+
hypothesis: text(item.hypothesis, `${path}.hypothesis`),
|
|
23
|
+
outcome,
|
|
24
|
+
confidence,
|
|
25
|
+
explanation: text(item.explanation, `${path}.explanation`),
|
|
26
|
+
isConfirmedFinding: item.isConfirmedFinding,
|
|
27
|
+
participatingAgents: Object.freeze(item.participatingAgents.map((a, i) => text(a, `${path}.participatingAgents[${i}]`))),
|
|
28
|
+
evidenceIds: Object.freeze(item.evidenceIds.map((e, i) => text(e, `${path}.evidenceIds[${i}]`))),
|
|
29
|
+
...("uncertaintyReason" in item && typeof item.uncertaintyReason === "string" ? { uncertaintyReason: item.uncertaintyReason } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function parseSimulationResults(value) {
|
|
33
|
+
const input = record(value, "simulationResults", [
|
|
34
|
+
"simulationId", "observations", "investigations", "findings",
|
|
35
|
+
]);
|
|
36
|
+
const simulationId = readSimulationId(input.simulationId, "simulationResults.simulationId");
|
|
37
|
+
if (!Array.isArray(input.observations)) {
|
|
38
|
+
throw new ContractValidationError("simulationResults.observations", "Expected an array.");
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(input.investigations)) {
|
|
41
|
+
throw new ContractValidationError("simulationResults.investigations", "Expected an array.");
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(input.findings)) {
|
|
44
|
+
throw new ContractValidationError("simulationResults.findings", "Expected an array.");
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
simulationId,
|
|
48
|
+
observations: Object.freeze(input.observations.map(obs => parseObservationSummary(obs))),
|
|
49
|
+
investigations: Object.freeze(input.investigations.map(inv => parseInvestigationSummary(inv))),
|
|
50
|
+
findings: Object.freeze(input.findings.map((f, i) => parseFindingAssessment(f, `simulationResults.findings[${i}]`))),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { oneOf, record, text } from "./validation.js";
|
|
3
|
+
export const userProfileIds = [
|
|
4
|
+
"explorer",
|
|
5
|
+
"impatient",
|
|
6
|
+
"beginner",
|
|
7
|
+
"regular",
|
|
8
|
+
"meticulous",
|
|
9
|
+
"distrustful",
|
|
10
|
+
];
|
|
11
|
+
export const defaultExecutionLimits = Object.freeze({
|
|
12
|
+
maxTurnsPerAgent: 3, maxInferenceCallsPerTurn: 12, inferenceTimeoutMs: 15_000, durationMs: 30_000,
|
|
13
|
+
});
|
|
14
|
+
export function parseExecutionLimits(value) {
|
|
15
|
+
const input = record(value, "setup.limits", Object.keys(defaultExecutionLimits));
|
|
16
|
+
function positive(key, maximum) {
|
|
17
|
+
const result = input[key];
|
|
18
|
+
if (typeof result !== "number" || !Number.isSafeInteger(result) || result < 1 || result > maximum) {
|
|
19
|
+
throw new ContractValidationError(`setup.limits.${key}`, `Expected an integer from 1 to ${maximum}.`);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
const limits = Object.freeze({ maxTurnsPerAgent: positive("maxTurnsPerAgent", 100),
|
|
24
|
+
maxInferenceCallsPerTurn: positive("maxInferenceCallsPerTurn", 100),
|
|
25
|
+
inferenceTimeoutMs: positive("inferenceTimeoutMs", 2_147_483_647), durationMs: positive("durationMs", 2_147_483_647) });
|
|
26
|
+
if (limits.inferenceTimeoutMs > limits.durationMs) {
|
|
27
|
+
throw new ContractValidationError("setup.limits.inferenceTimeoutMs", "Inference timeout must not exceed the session duration.");
|
|
28
|
+
}
|
|
29
|
+
return limits;
|
|
30
|
+
}
|
|
31
|
+
function targetUrl(value) {
|
|
32
|
+
const input = text(value, "setup.targetUrl").trim();
|
|
33
|
+
try {
|
|
34
|
+
if (!/^https?:\/\//i.test(input))
|
|
35
|
+
throw new Error("Expected an absolute HTTP address.");
|
|
36
|
+
const parsed = new URL(input);
|
|
37
|
+
if (!parsed.hostname || parsed.username || parsed.password)
|
|
38
|
+
throw new Error("Invalid or credential-bearing address.");
|
|
39
|
+
return parsed.href;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ContractValidationError("setup.targetUrl", "Expected an absolute HTTP(S) URL without embedded credentials.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function parseSimulationSetup(value) {
|
|
46
|
+
const input = record(value, "setup", ["name", "targetUrl", "environment", "goal", "userProfileIds", "limits"]);
|
|
47
|
+
const environment = record(input.environment, "setup.environment", ["kind", "deployment"]);
|
|
48
|
+
if (!Array.isArray(input.userProfileIds) || !input.userProfileIds.length) {
|
|
49
|
+
throw new ContractValidationError("setup.userProfileIds", "Select at least one behavioral profile.");
|
|
50
|
+
}
|
|
51
|
+
const profiles = Array.from(input.userProfileIds, (id) => oneOf(id, userProfileIds, "setup.userProfileIds"));
|
|
52
|
+
if (new Set(profiles).size !== profiles.length)
|
|
53
|
+
throw new ContractValidationError("setup.userProfileIds", "Profiles must be unique.");
|
|
54
|
+
return Object.freeze({ ...(input.name === undefined ? {} : { name: text(input.name, "setup.name").trim() }),
|
|
55
|
+
targetUrl: targetUrl(input.targetUrl), goal: text(input.goal, "setup.goal"),
|
|
56
|
+
environment: Object.freeze({ kind: oneOf(environment.kind, ["web"], "setup.environment.kind"),
|
|
57
|
+
deployment: oneOf(environment.deployment, ["local", "development", "test", "staging", "production"], "setup.environment.deployment") }),
|
|
58
|
+
userProfileIds: Object.freeze(profiles),
|
|
59
|
+
limits: parseExecutionLimits(input.limits === undefined ? defaultExecutionLimits : input.limits), });
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { oneOf, readAgentId, readSimulationId, record, text } from "./validation.js";
|
|
3
|
+
function lifecycle(value, statuses, path) {
|
|
4
|
+
const input = record(value, path, ["status", "reason"]);
|
|
5
|
+
const status = oneOf(input.status, statuses, `${path}.status`);
|
|
6
|
+
if (status === "failed") {
|
|
7
|
+
return Object.freeze({ status: "failed", reason: text(input.reason, `${path}.reason`) });
|
|
8
|
+
}
|
|
9
|
+
if (Object.hasOwn(input, "reason")) {
|
|
10
|
+
throw new ContractValidationError(`${path}.reason`, "Only a failed lifecycle carries a reason.");
|
|
11
|
+
}
|
|
12
|
+
return Object.freeze({ status: status });
|
|
13
|
+
}
|
|
14
|
+
export function parseAgentSummary(value) {
|
|
15
|
+
const input = record(value, "agent", ["id", "simulationId", "name", "lifecycle"]);
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
id: readAgentId(input.id, "agent.id"),
|
|
18
|
+
simulationId: readSimulationId(input.simulationId, "agent.simulationId"),
|
|
19
|
+
name: text(input.name, "agent.name"),
|
|
20
|
+
lifecycle: lifecycle(input.lifecycle, ["ready", "running", "completed", "failed", "cancelled"], "agent.lifecycle"),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function parseSimulationSummary(value) {
|
|
24
|
+
const input = record(value, "simulation", ["id", "environment", "lifecycle", "agentIds"]);
|
|
25
|
+
const environment = record(input.environment, "simulation.environment", ["kind", "deployment"]);
|
|
26
|
+
if (!Array.isArray(input.agentIds)) {
|
|
27
|
+
throw new ContractValidationError("simulation.agentIds", "Expected an array.");
|
|
28
|
+
}
|
|
29
|
+
const agentIds = Array.from(input.agentIds, (id, index) => readAgentId(id, `simulation.agentIds[${index}]`));
|
|
30
|
+
if (new Set(agentIds).size !== agentIds.length) {
|
|
31
|
+
throw new ContractValidationError("simulation.agentIds", "Agent IDs must be unique.");
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
id: readSimulationId(input.id, "simulation.id"),
|
|
35
|
+
environment: Object.freeze({
|
|
36
|
+
kind: oneOf(environment.kind, ["web"], "simulation.environment.kind"),
|
|
37
|
+
deployment: oneOf(environment.deployment, ["local", "development", "test", "staging", "production"], "simulation.environment.deployment"),
|
|
38
|
+
}),
|
|
39
|
+
lifecycle: lifecycle(input.lifecycle, ["created", "running", "completed", "failed", "cancelled"], "simulation.lifecycle"),
|
|
40
|
+
agentIds: Object.freeze(agentIds),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { oneOf, record, text } from "./validation.js";
|
|
3
|
+
/** Restrict explicit connection checks to local HTTP services, without DNS. */
|
|
4
|
+
export function parseLocalTargetUrl(value) {
|
|
5
|
+
const input = text(value, "target.url").trim();
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(input);
|
|
8
|
+
if (input.length > 2048 || !/^https?:\/\//i.test(input) || url.username || url.password ||
|
|
9
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
|
|
10
|
+
throw new Error();
|
|
11
|
+
url.hash = "";
|
|
12
|
+
return url.href;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new ContractValidationError("target.url", "Use a local HTTP(S) URL with localhost, 127.0.0.1, or [::1], without credentials.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function parseTargetSuggestion(value) {
|
|
19
|
+
const input = record(value, "targetSuggestion", ["url", "source", "scriptName"]);
|
|
20
|
+
return Object.freeze({ url: parseLocalTargetUrl(input.url), source: oneOf(input.source, ["vite-default", "vite-script"], "targetSuggestion.source"),
|
|
21
|
+
scriptName: text(input.scriptName, "targetSuggestion.scriptName") });
|
|
22
|
+
}
|
|
23
|
+
export function parseTargetConnection(value) {
|
|
24
|
+
const input = record(value, "connection", ["url", "kind", "statusCode"]);
|
|
25
|
+
const url = parseLocalTargetUrl(input.url);
|
|
26
|
+
const kind = oneOf(input.kind, ["response", "unreachable", "timeout", "tls-error", "busy"], "connection.kind");
|
|
27
|
+
if (kind === "response") {
|
|
28
|
+
if (typeof input.statusCode !== "number" || !Number.isInteger(input.statusCode) || input.statusCode < 100 || input.statusCode > 599) {
|
|
29
|
+
throw new ContractValidationError("connection.statusCode", "Expected an HTTP status code.");
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({ url, kind, statusCode: input.statusCode });
|
|
32
|
+
}
|
|
33
|
+
if (input.statusCode !== undefined)
|
|
34
|
+
throw new ContractValidationError("connection.statusCode", "Only HTTP responses have a status code.");
|
|
35
|
+
return Object.freeze({ url, kind });
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { agentId, simulationId } from "../domain/index.js";
|
|
2
|
+
import { ContractValidationError } from "./errors.js";
|
|
3
|
+
export function record(value, path, keys) {
|
|
4
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
5
|
+
throw new ContractValidationError(path, "Expected a plain object.");
|
|
6
|
+
}
|
|
7
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
9
|
+
throw new ContractValidationError(path, "Expected a plain object.");
|
|
10
|
+
}
|
|
11
|
+
if (Reflect.ownKeys(value).some((key) => typeof key !== "string" || !keys.includes(key))) {
|
|
12
|
+
throw new ContractValidationError(path, "Unknown fields are not supported by this schema.");
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function text(value, path) {
|
|
17
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
18
|
+
throw new ContractValidationError(path, "Expected a nonblank string.");
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export function oneOf(value, values, path) {
|
|
23
|
+
if (typeof value !== "string" || !values.includes(value)) {
|
|
24
|
+
throw new ContractValidationError(path, "Unsupported value.");
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function readAgentId(value, path) {
|
|
29
|
+
return agentId(text(value, path));
|
|
30
|
+
}
|
|
31
|
+
export function readSimulationId(value, path) {
|
|
32
|
+
return simulationId(text(value, path));
|
|
33
|
+
}
|
|
34
|
+
export function timestamp(value, path) {
|
|
35
|
+
const result = text(value, path);
|
|
36
|
+
const date = new Date(result);
|
|
37
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result) ||
|
|
38
|
+
!Number.isFinite(date.getTime()) || date.toISOString() !== result) {
|
|
39
|
+
throw new ContractValidationError(path, "Expected a valid UTC timestamp in YYYY-MM-DDTHH:mm:ss.sssZ format.");
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DomainError, nonBlank } from "./errors.js";
|
|
2
|
+
import { agentId } from "./identity.js";
|
|
3
|
+
import { createAgentProfile } from "./profile.js";
|
|
4
|
+
export function createAgent(id, profile) {
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
id: agentId(id),
|
|
7
|
+
profile: createAgentProfile(profile),
|
|
8
|
+
lifecycle: Object.freeze({ status: "ready" }),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function transitionAgent(agent, next) {
|
|
12
|
+
const current = agent.lifecycle.status;
|
|
13
|
+
const allowed = current === "ready"
|
|
14
|
+
? next.status === "running" || next.status === "cancelled"
|
|
15
|
+
: current === "running" && ["completed", "failed", "cancelled"].includes(next.status);
|
|
16
|
+
if (!allowed) {
|
|
17
|
+
throw new DomainError("INVALID_TRANSITION", `Agent cannot move from ${current} to ${next.status}.`);
|
|
18
|
+
}
|
|
19
|
+
const lifecycle = next.status === "failed"
|
|
20
|
+
? Object.freeze({ status: "failed", reason: nonBlank(next.reason, "Failure reason") })
|
|
21
|
+
: Object.freeze({ status: next.status });
|
|
22
|
+
return Object.freeze({ ...agent, lifecycle });
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class DomainError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "DomainError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function nonBlank(value, field) {
|
|
10
|
+
if (value.trim().length === 0) {
|
|
11
|
+
throw new DomainError("INVALID_VALUE", `${field} must not be blank.`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { nonBlank } from "./errors.js";
|
|
2
|
+
export function simulationId(value) {
|
|
3
|
+
return nonBlank(value, "Simulation ID");
|
|
4
|
+
}
|
|
5
|
+
export function agentId(value) {
|
|
6
|
+
return nonBlank(value, "Agent ID");
|
|
7
|
+
}
|
|
8
|
+
export function observationId(value) {
|
|
9
|
+
return nonBlank(value, "Observation ID");
|
|
10
|
+
}
|
|
11
|
+
export function evidenceId(value) {
|
|
12
|
+
return nonBlank(value, "Evidence ID");
|
|
13
|
+
}
|
|
14
|
+
export function investigationId(value) {
|
|
15
|
+
return nonBlank(value, "Investigation ID");
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { DomainError } from "./errors.js";
|
|
2
|
+
export { agentId, simulationId } from "./identity.js";
|
|
3
|
+
export { observationId, evidenceId } from "./identity.js";
|
|
4
|
+
export { createObservation } from "./observation.js";
|
|
5
|
+
export { investigationId } from "./identity.js";
|
|
6
|
+
export { createInvestigationResult } from "./investigation-result.js";
|
|
7
|
+
export { requestInvestigation, assignInvestigation, completeInvestigation } from "./investigation.js";
|
|
8
|
+
export { createAgentProfile } from "./profile.js";
|
|
9
|
+
export { createAgent, transitionAgent } from "./agent.js";
|
|
10
|
+
export { createSimulation, transitionSimulation, updateSimulationAgent } from "./simulation.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DomainError, nonBlank } from "./errors.js";
|
|
2
|
+
import { evidenceId } from "./identity.js";
|
|
3
|
+
export function createInvestigationResult(input) {
|
|
4
|
+
if (!["supported", "contradicted", "inconclusive"].includes(input.outcome)) {
|
|
5
|
+
throw new DomainError("INVALID_VALUE", "Unknown investigation outcome.");
|
|
6
|
+
}
|
|
7
|
+
const evidenceIds = input.evidenceIds.map(evidenceId);
|
|
8
|
+
if (new Set(evidenceIds).size !== evidenceIds.length) {
|
|
9
|
+
throw new DomainError("INVALID_VALUE", "Result evidence references must be unique.");
|
|
10
|
+
}
|
|
11
|
+
if (input.outcome !== "inconclusive" && evidenceIds.length === 0) {
|
|
12
|
+
throw new DomainError("INVALID_VALUE", "A supported or contradicted hypothesis requires evidence references.");
|
|
13
|
+
}
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
outcome: input.outcome,
|
|
16
|
+
explanation: nonBlank(input.explanation, "Result explanation"),
|
|
17
|
+
evidenceIds: Object.freeze(evidenceIds),
|
|
18
|
+
});
|
|
19
|
+
}
|