diff --git a/.github/actions/prune-repository/prune.py b/.github/actions/prune-repository/prune.py index 400859f7b6b..bfb5a60564d 100644 --- a/.github/actions/prune-repository/prune.py +++ b/.github/actions/prune-repository/prune.py @@ -57,6 +57,7 @@ "libs/@hashintel/brunch-agent/AGENTS.md", "libs/@hashintel/brunch-agent/CONTEXT.md", "libs/@hashintel/brunch-agent/docs", + "libs/@hashintel/brunch-agent/evaluations", "libs/@hashintel/brunch-agent/scripts", ], } diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 2d47e9c6681..e34c81dff00 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -36,6 +36,6 @@ "**/.cursor/plans/**", "libs/@hashintel/ds-components/**", "libs/@hashintel/ds-helpers/**", - "libs/@hashintel/brunch-agent/docs/**" + "libs/@hashintel/brunch-agent/**/*.md" ] } diff --git a/libs/@hashintel/brunch-agent/AGENTS.md b/libs/@hashintel/brunch-agent/AGENTS.md index f38e3481753..9e6a4226a8c 100644 --- a/libs/@hashintel/brunch-agent/AGENTS.md +++ b/libs/@hashintel/brunch-agent/AGENTS.md @@ -10,6 +10,7 @@ guidance always wins where it conflicts with this file. - `packages/transport-*`: wire transports; none may depend on a binding. - `packages/plugin-*`: target plugins; each depends only on the harness. - `../../../apps/brunch-agent`: remote server, application composition, and local diagnostics. +- `evaluations`: cases, protocols, and oracles; see `evaluations/AGENTS.md` before changing them. The context root is not a package-manager root. Do not add a `package.json`, lockfile, nested workspace configuration, or standalone CI here. Run package tasks through HASH's root Yarn/Turbo @@ -25,24 +26,19 @@ workspace. - Use Graphite (`gt`) for stack operations; do not use `gh stack` in HASH. - Issues live in Linear team `FE`, project `brunch-agent`. -- Keep the human-owned issue contract separate from collapsed `πŸ—οΈ Agent notes`; see - `docs/agents/issue-writing.md`. -- Maintain the glossary in `CONTEXT.md` and context decisions in `docs/adr/`; see - `docs/agents/domain.md`. -- Keep `docs/INDEX.md` complete and follow `docs/agents/documentation.md`. -- Before closing a Brunch work arc, run the context-local `arc-close` skill and its canonical - procedure in `docs/agents/arc-close.md`. -- At design moments involving Flue, follow `docs/agents/flue-routing.md`. - -The complete protocol set is: - -- `docs/agents/arc-close.md` -- `docs/agents/documentation.md` -- `docs/agents/domain.md` -- `docs/agents/flue-routing.md` -- `docs/agents/git-workflow.md` -- `docs/agents/issue-tracker.md` -- `docs/agents/issue-writing.md` -- `docs/agents/legibility.md` -- `docs/agents/posture.md` -- `docs/agents/triage-labels.md` +- Brunch Markdown is created and maintained by agents; exclude it from `oxfmt` and `markdownlint`. + +Route by trigger; load only the applicable compact protocol: + +- Start or resume without a proof target, or when objectives, pressure, proof, authority, external + gates, frontier value, or arc-close findings change: `docs/agents/steering.md`. +- Create, mutate, triage, or structure issues: `docs/agents/issue-tracker.md`, + `docs/agents/issue-writing.md`, and `docs/agents/triage-labels.md`. +- Add, move, settle, or index documents: `docs/agents/documentation.md`. +- Change domain terms or accepted context decisions: `docs/agents/domain.md`. +- Make a Flue design choice: `docs/agents/flue-routing.md`. +- Produce a significant agent-authored artifact or proof: `docs/agents/legibility.md`. +- Make an architecture-sensitive move: `docs/agents/posture.md`. +- Operate on branches, stacks, commits, or PRs: `docs/agents/git-workflow.md`. +- Close a work arc: run the context-local `arc-close` skill and + `docs/agents/arc-close.md`. diff --git a/libs/@hashintel/brunch-agent/CONTEXT.md b/libs/@hashintel/brunch-agent/CONTEXT.md index 2dfdb637fde..45c61d4386f 100644 --- a/libs/@hashintel/brunch-agent/CONTEXT.md +++ b/libs/@hashintel/brunch-agent/CONTEXT.md @@ -71,7 +71,7 @@ _Avoid_: digest (brunch's form) ### Envelope & packs **Intermediate representation (IR)**: -The elicited conceptual model a target-document accumulates β€” the middle of three registers (ADR-0003): typed **assertions** (active captures) are folded by a pure, plugin-declared fold into the **model** (the IR proper β€” node instances with slot states), which **projections** consume without rereading the transcript. Not a second store β€” the model is a derivation, recomputable from active captures at any time, never a persistence surface; the rendered artifact is one projection of the model, never the model itself. Defining a plugin's IR means writing its contract β€” model schema, proposal catalog, fold table, demand table (`plugin-contract-spec.md`, provisional). An earlier definition read the capture set itself as the IR; ADR-0003 amends it. +The elicited conceptual model a target-document accumulates β€” the middle of three registers (ADR-0003): typed **assertions** (active captures) are folded by a pure, plugin-declared fold into the **model** (the IR proper β€” node instances with slot states), which **projections** consume without rereading the transcript. Not a second store β€” the model is a derivation, recomputable from active captures at any time, never a persistence surface; the rendered artifact is one projection of the model, never the model itself. Defining a plugin's IR means writing its contract β€” model schema, proposal catalog, fold table, demand table (`docs/specs/plugin-contract.md`, provisional). An earlier definition read the capture set itself as the IR; ADR-0003 amends it. _Avoid_: knowledge store, domain model (as a stored unit), staging area **Capture envelope**: @@ -84,7 +84,7 @@ A capture's provenance link: a **quoted excerpt** (primary, the model-facing cit `explicit | inferred | tentative | defaulted | external-lookup` β€” how a capture's content relates to what the user actually said. Distinct from confidence; excluded from capture identity. One status per capture, coupled structurally to the provenance shape (see Basis) β€” per-field status is unrepresentable by design (FE-1390; FE-1405's central input, consumed without amendment: the structure that wanted per-field status lives below the status, in proposal interiors). **Grade**: -How narrow a slot value's interpretation space is β€” "fewer readings remain." Per-slot orderings read by the fold, promotion, and demands ("this anchor demands the `range` rung"). Never claim strength: that is confidence (`firm | hedged | speculative`, envelope-side, orthogonal by design). Two sources: **form grades** from the standard-interiors library's ladders (e.g. verbal < point < range < quantiles), **composition grades** plugin-declared (e.g. Gherkin's given-only < given-when < full-gwt). Coined by the FE-1405 arc (`plugin-contract-spec.md`, provisional). +How narrow a slot value's interpretation space is β€” "fewer readings remain." Per-slot orderings read by the fold, promotion, and demands ("this anchor demands the `range` rung"). Never claim strength: that is confidence (`firm | hedged | speculative`, envelope-side, orthogonal by design). Two sources: **form grades** from the standard-interiors library's ladders (e.g. verbal < point < range < quantiles), **composition grades** plugin-declared (e.g. Gherkin's given-only < given-when < full-gwt). Coined by the FE-1405 arc (`docs/specs/plugin-contract.md`, provisional). _Avoid_: confidence (for narrowing), precision (unqualified) **Basis**: diff --git a/libs/@hashintel/brunch-agent/README.md b/libs/@hashintel/brunch-agent/README.md index 2e6bbc3eb89..28708deac2a 100644 --- a/libs/@hashintel/brunch-agent/README.md +++ b/libs/@hashintel/brunch-agent/README.md @@ -6,8 +6,10 @@ This directory is its context and agent-session root, not a package workspace: - [`CONTEXT.md`](./CONTEXT.md) defines the domain language. - [`docs/adr/`](./docs/adr/) records governing decisions. -- [`docs/spec.md`](./docs/spec.md) defines the harness contract. +- [`docs/specs/elicitation-kernel.md`](docs/specs/elicitation-kernel.md) defines the harness contract. - [`docs/INDEX.md`](./docs/INDEX.md) indexes Brunch documentation. +- [`docs/control/STEERING.md`](./docs/control/STEERING.md) holds current strategic truth; + [`STRATEGY-LOG.md`](./docs/control/STRATEGY-LOG.md) records material strategic decisions. - [`packages/core/`](./packages/core/) is `@hashintel/brunch-agent`. - [`packages/binding-flue/`](./packages/binding-flue/) is the Flue binding. - [`packages/transport-aisdk/`](./packages/transport-aisdk/) is the AI SDK transport. diff --git a/libs/@hashintel/brunch-agent/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index ca6ea309c02..aca4b6325c5 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -6,9 +6,15 @@ settlement) Β· `active` (artifact of a live effort) Β· `settled` (permanent home (retained history replaced by newer canon) Β· `accepted` (ratified ADR) Β· `external` (canonical copy lives outside the repo). +The authoritative role topology is defined by the documentation protocol. The role-based zones +below are authoritative. Agent protocols are registered through `AGENTS.md`; the strategic +control loop is [`docs/agents/steering.md`](agents/steering.md). + ## Inbox (awaiting settlement) -_(empty β€” items settle out via the arc-close inbox sweep)_ +| Document | Status | Date | Digest | Used by | +| ---------------------------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | +| [SDCPN JSON nets](inbox/SDCPNs/) | inbox | 2026-08-24 | Seven valid Petrinaut v1 files: two plain-net gas variants, gas SPN/CPN/DCPN, truck, and semiconductor; a prior `gases-5` reference is unconfirmed and likely erroneous, not part of the corpus | Frontier 0 corpus inventory; settlement/conformance pending | ## Reference (settled sources) @@ -23,67 +29,68 @@ _(empty β€” items settle out via the arc-close inbox sweep)_ | [amp-analysis-flue-vs-tilde](reference/amp-analysis-flue-vs-tilde.md) | settled | 2026-08-14 | Amp thread export: comparative assessment of the Flue and tilde agent frameworks (development and deployment stories) and its import for this project; verdict: keep Flue, Tilde is a control plane not a runtime | reconciled into flue-architecture-cheatsheet (2026-08-17); source of the pre-remote-exposure gates | | [2026-08 SDCPNs for cyber-physical systems](reference/2026-08%20SDCPNs%20for%20cyber-physical%20systems.md) | settled | 2026-08 (settled 2026-08-18) | Unattributed draft blog post (image placeholders, typos): five-level SDCPN explainer applied to gas supply, truck fleet, semiconductor fab; arrived during the FE-1405 arc. Read skeptically: good pedagogy, promotional register β€” concedes its formal guarantees don't apply once continuous/stochastic features are used (open research problem), models carry heavy kernel/guard logic that strains the "formal and inspectable" claim, and Petrinaut's integrator limitation is admitted | Register-3 background (projection-target expressivity) only; not elicitation design input; no consumer yet | -## history/planning/elicitation-kernel (effort complete 2026-08-10; settled 2026-08-12) +## Specifications and archived elicitation-kernel record | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [spec.md](spec.md) | settled | linked from FE-1366 (context-canonical) | The elicitation-kernel spec: 14 sections + adjudications; FE-1437 import amendment records the native HASH package family, context root, and remote-server application charter | -| [product-description.md](history/planning/elicitation-kernel/product-description.md) | settled | none | STE-style product description | -| [product-description-plain.md](history/planning/elicitation-kernel/product-description-plain.md) | settled | none | Plain-prose rendering of the product description | -| [map.md](history/planning/elicitation-kernel/map.md) | settled | **mirrored in full**: FE-1366 | Completed wayfinder map | -| [issues/](history/planning/elicitation-kernel/issues/) 01–13 | settled | **mirrored in full**: FE-1367–FE-1379 (relations preserved) | 13 resolved tickets | -| [notes/consistency-prepass](history/planning/elicitation-kernel/notes/consistency-prepass-2026-08-10.md) | settled | none | Pre-assembly contradiction audit (7 contradictions, adjudicated in spec Appendix A) | +| [spec.md](specs/elicitation-kernel.md) | settled | linked from FE-1366 (context-canonical) | The elicitation-kernel spec: 14 sections + adjudications; FE-1437 import amendment records the native HASH package family, context root, and remote-server application charter | +| [product-description.md](archive/elicitation-kernel/product-description.md) | settled | none | STE-style product description | +| [product-description-plain.md](archive/elicitation-kernel/product-description-plain.md) | settled | none | Plain-prose rendering of the product description | +| [map.md](archive/elicitation-kernel/map.md) | settled | **mirrored in full**: FE-1366 | Completed wayfinder map | +| [issues/](archive/elicitation-kernel/issues) 01–13 | settled | **mirrored in full**: FE-1367–FE-1379 (relations preserved) | 13 resolved tickets | +| [notes/consistency-prepass](archive/elicitation-kernel/notes/consistency-prepass-2026-08-10.md) | settled | none | Pre-assembly contradiction audit (7 contradictions, adjudicated in spec Appendix A) | -## planning/process-model-elicitation (effort active β€” FE-1357) +## Process-model elicitation artifacts (FE-1357) | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [recommendation-demo-vehicle](planning/process-model-elicitation/recommendation-demo-vehicle.md) | superseded | linked from FE-1362/1328/1329/1331/1333 | Demo-vehicle recommendation: demo shell + artifact boundary; superseded by ADR-0004 (18 Aug meeting chose in-Petrinaut staging) | -| [petrinaut-integration-spec](planning/process-model-elicitation/petrinaut-integration-spec.md) | active | FE-1433 | Integration spec: elicitor as remote server behind the `aiAssistant` transport; suspension-borne client tools; `transport-aisdk`; principal + owner key; two gating spikes | -| [FE-1434 suspension verdict](planning/process-model-elicitation/spikes/fe-1434-suspension-verdict-2026-08-19.md) | active | FE-1434 | Flue 2.0.3 carries a terminating client-tool batch through one durable pending slot and one non-user result signal; 3- and 100-result cases preserve ids in two dispatches | -| [FE-1434 suspension evidence](planning/process-model-elicitation/spikes/fe-1434-suspension-evidence-2026-08-19.json) | active | FE-1434 | Deterministic transcript from the faux-provider runtime probe: native tool-result admission refused, signal resume succeeds, returned text is non-user and uncitable | -| [adapter-panel-spike-2026-08-19](planning/process-model-elicitation/adapter-panel-spike-2026-08-19.md) | settled | FE-1435 | Real-panel spike verdict: AI SDK v6 SSE drives Petrinaut text/reasoning, default server-tool summaries, two live-editor client tools in one batched follow-up, and the diagnostics decorator; full POST/SSE transcript frozen as golden fixtures | -| [transport-aisdk-implementation-2026-08-19](planning/process-model-elicitation/transport-aisdk-implementation-2026-08-19.md) | settled | FE-1436 | Durable real-panel transport: application `/api/chat` endpoint, substrate-neutral harness reply events, AI SDK v6 encoding, boundary gates, opt-in protocol inspection, and a clean-checkout local Petrinaut launcher | -| [ask-return-implementation-2026-08-19](planning/process-model-elicitation/ask-return-implementation-2026-08-19.md) | active | FE-1449 | Ask suspend/return over the wire: the ask leaves as an awaiting client tool, the correlated `{ answer }` submission is admitted against durable history and resumes the conversation; stale/forged/duplicate/non-ask outputs refused before dispatch | -| [notes/grilling-inputs-2026-08-12](planning/process-model-elicitation/notes/grilling-inputs-2026-08-12.md) | active | referenced from map | Session carryover: destination trend, facets/motions, Dora-checklist validate table | -| [notes/expert-meeting-prep](planning/process-model-elicitation/notes/expert-meeting-prep-2026-08-11.md) | active | referenced from map | Prep brief for the Yannis meeting | -| [notes/expert-meeting-findings](planning/process-model-elicitation/notes/expert-meeting-findings-2026-08-11.md) | active | referenced from map | Meeting findings: 5 facts, 7 idea seeds, 2 commitments | -| [notes/open-questions-elicitation-design](planning/process-model-elicitation/notes/open-questions-elicitation-design-2026-08-11.md) | superseded | β€” | Draft; canonical copy is the [Notion page](https://www.notion.so/hashintel/3b93c81fe024801b89b3cf63a9a6ff20) (`external`) | -| [research/petrinaut-survey](planning/process-model-elicitation/research/petrinaut-survey.md) | active | gisted in FE-1358 resolution | Petrinaut architecture/assistant/format survey + coupling audit | -| [research/voice-feasibility](planning/process-model-elicitation/research/voice-feasibility.md) | active | gisted in FE-1359 resolution | Voice verdict: bolt-on with constraints; T0–T3 tiers | -| [research/elicitation-strategy-literature](planning/process-model-elicitation/research/elicitation-strategy-literature.md) | active | gisted in FE-1360 resolution | Literature synthesis, 9 sections, verification-labeled | -| [research/re-interviewing-literature-worker-report](planning/process-model-elicitation/research/re-interviewing-literature-worker-report.md) | active | noted on FE-1361 | Verbatim instruments: 34-mistake taxonomy, question typologies, LLM-interviewer results | -| [baseline/](planning/process-model-elicitation/baseline/) | active | gisted in FE-1361 resolution | Baseline-control experiment: protocol, situation pack, v0 prompt, runner, both transcripts, scored read-out | -| [ir-design](planning/process-model-elicitation/ir-design.md) | active | gisted in FE-1364 resolution | The IR design: Layer A (ratified on worked examples, FE-1397; definition sentence amended by ADR-0003) + the CPS plugin's ten-kind payload (Layer B) | -| [ir-worked-examples](planning/process-model-elicitation/ir-worked-examples.md) | active | gisted in FE-1397 | Layer-A validation across Gherkin/CPS/BPMN + assurance: property verdicts, amendments, sublimation findings | -| [ir-design-plain](planning/process-model-elicitation/ir-design-plain.md) | active | strain findings on FE-1401 | Plain-prose rendering of the IR design; the rendering pass doubled as review (7 strain findings, one load-bearing) | -| [notes/research-patterns-audit](planning/process-model-elicitation/notes/research-patterns-audit.md) | active | FE-1401 / card inputs on FE-1403 | Plain-language audit of ~30 research imports in 7 families, evidence-graded, with an 8-point strain appendix | -| [notes/penciled-directions-2026-08-14](planning/process-model-elicitation/notes/penciled-directions-2026-08-14.md) | active | FE-1401 | Penciled directions from the legibility session: 8 items with firming actions + editorial reflections | -| [capture-store-plain](planning/process-model-elicitation/capture-store-plain.md) | active | strain findings on FE-1401 | STE-leaning rendering of the capture-store semantics (FE-1390/FE-1389) with a load-bearing not-guaranteed section; 8-point strain report incl. two command-reachable unclosable-conflict paths (confirms FE-1419 commits 7/8) and the FE-1405 status-arity answer | -| [notes/deep-read-fe-1389](planning/process-model-elicitation/notes/deep-read-fe-1389.md) | active | FE-1401 / findings in FE-1420 | Deep-read of the walking skeleton: builder's account, spec-discharge table (issues 10/13 capabilities discharged; markdown floor contradicted in the UI), 12 findings; source of PR #10's backfilled record | -| [notes/deep-read-fe-1390](planning/process-model-elicitation/notes/deep-read-fe-1390.md) | active | FE-1401 / probes on FE-1419 | Deep-read of the capture store: spec-discharge table, write-time tiering assessment (penciled item 7), the FE-1405 status-arity answer, and live-probed confirmation of FE-1419's capture-store claims plus one new aliasing hole; source of PR #11's backfilled record | -| [plugin-contract-spec](planning/process-model-elicitation/plugin-contract-spec.md) | active | FE-1431 (spec issue); decided on FE-1405 | Provisional spec: a plugin is two schemas and two tables (model schema, proposal catalog, fold table, demand table) over the three-register IR (ADR-0003) β€” harness-machinery typology, standard-interiors library, grade-as-narrowing, derived fold rules; strains 4–7 and envelope pressure #2 held open with owners | - -## planning/\_shared (cross-effort control documents) +| [recommendation-demo-vehicle](archive/decisions/superseded/recommendation-demo-vehicle.md) | superseded | linked from FE-1362/1328/1329/1331/1333 | Demo-vehicle recommendation: demo shell + artifact boundary; superseded by ADR-0004 (18 Aug meeting chose in-Petrinaut staging) | +| [petrinaut-integration-spec](specs/petrinaut-integration.md) | active | FE-1433 | Integration spec: elicitor as remote server behind the `aiAssistant` transport; suspension-borne client tools; `transport-aisdk`; principal + owner key; two gating spikes | +| [FE-1434 suspension verdict](evidence/proofs/spikes/fe-1434-suspension-verdict-2026-08-19.md) | active | FE-1434 | Flue 2.0.3 carries a terminating client-tool batch through one durable pending slot and one non-user result signal; 3- and 100-result cases preserve ids in two dispatches | +| [FE-1434 suspension evidence](evidence/proofs/spikes/fe-1434-suspension-evidence-2026-08-19.json) | active | FE-1434 | Deterministic transcript from the faux-provider runtime probe: native tool-result admission refused, signal resume succeeds, returned text is non-user and uncitable | +| [adapter-panel-spike-2026-08-19](evidence/proofs/spikes/fe-1435-adapter-panel-2026-08-19.md) | settled | FE-1435 | Real-panel spike verdict: AI SDK v6 SSE drives Petrinaut text/reasoning, default server-tool summaries, two live-editor client tools in one batched follow-up, and the diagnostics decorator; full POST/SSE transcript frozen as golden fixtures | +| [transport-aisdk-implementation-2026-08-19](evidence/proofs/implementations/fe-1436-transport-aisdk-2026-08-19.md) | settled | FE-1436 | Durable real-panel transport: application `/api/chat` endpoint, substrate-neutral harness reply events, AI SDK v6 encoding, boundary gates, opt-in protocol inspection, and a clean-checkout local Petrinaut launcher | +| [ask-return-implementation-2026-08-19](evidence/proofs/implementations/fe-1449-ask-return-2026-08-19.md) | active | FE-1449 | Ask suspend/return over the wire: the ask leaves as an awaiting client tool, the correlated `{ answer }` submission is admitted against durable history and resumes the conversation; stale/forged/duplicate/non-ask outputs refused before dispatch | +| [notes/grilling-inputs-2026-08-12](archive/planning-inputs/grilling-inputs-2026-08-12.md) | settled | referenced from map | Session carryover: destination trend, facets/motions, Dora-checklist validate table | +| [notes/expert-meeting-prep](archive/meetings/expert-meeting-prep-2026-08-11.md) | settled | referenced from map | Prep brief for the Yannis meeting | +| [notes/expert-meeting-findings](evidence/proofs/research/expert-meeting-findings-2026-08-11.md) | active | referenced from map | Meeting findings: 5 facts, 7 idea seeds, 2 commitments | +| [notes/open-questions-elicitation-design](archive/external-snapshots/open-questions-elicitation-design-2026-08-11.md) | superseded | β€” | Draft; canonical copy is the [Notion page](https://www.notion.so/hashintel/3b93c81fe024801b89b3cf63a9a6ff20) (`external`) | +| [research/petrinaut-survey](reference/research/petrinaut-survey.md) | active | gisted in FE-1358 resolution | Petrinaut architecture/assistant/format survey + coupling audit | +| [research/voice-feasibility](reference/research/voice-feasibility.md) | active | gisted in FE-1359 resolution | Voice verdict: bolt-on with constraints; T0–T3 tiers | +| [research/elicitation-strategy-literature](reference/research/elicitation/elicitation-strategy-literature.md) | active | gisted in FE-1360 resolution | Literature synthesis, 9 sections, verification-labeled | +| [research/re-interviewing-literature-worker-report](reference/research/elicitation/interviewing-literature-source-catalog.md) | active | noted on FE-1361 | Verbatim instruments: 34-mistake taxonomy, question typologies, LLM-interviewer results | +| [baseline evaluation evidence](evidence/evaluations/process-model-elicitation/baseline/) | settled | gisted in FE-1361 resolution | Immutable baseline-control evidence: both transcripts, raw snapshots, delivered models, and scored read-out; executable cases and protocol live under `evaluations/` | +| [ir-design](specs/intermediate-representation.md) | active | gisted in FE-1364 resolution | The IR design: Layer A (ratified on worked examples, FE-1397; definition sentence amended by ADR-0003) + the CPS plugin's ten-kind payload (Layer B) | +| [ir-worked-examples](evidence/proofs/design/intermediate-representation-worked-examples.md) | active | gisted in FE-1397 | Layer-A validation across Gherkin/CPS/BPMN + assurance: property verdicts, amendments, sublimation findings | +| [ir-design-plain](specs/intermediate-representation-plain.md) | active | strain findings on FE-1401 | Plain-prose rendering of the IR design; the rendering pass doubled as review (7 strain findings, one load-bearing) | +| [notes/research-patterns-audit](evidence/proofs/audits/research-patterns-audit.md) | active | FE-1401 / card inputs on FE-1403 | Plain-language audit of ~30 research imports in 7 families, evidence-graded, with an 8-point strain appendix | +| [notes/penciled-directions-2026-08-14](archive/planning-inputs/penciled-directions-2026-08-14.md) | settled | FE-1401 | Penciled directions from the legibility session: 8 items with firming actions + editorial reflections | +| [capture-store-plain](reference/architecture/capture-store.md) | active | strain findings on FE-1401 | STE-leaning rendering of the capture-store semantics (FE-1390/FE-1389) with a load-bearing not-guaranteed section; 8-point strain report incl. two command-reachable unclosable-conflict paths (confirms FE-1419 commits 7/8) and the FE-1405 status-arity answer | +| [notes/deep-read-fe-1389](evidence/proofs/audits/deep-read-fe-1389.md) | active | FE-1401 / findings in FE-1420 | Deep-read of the walking skeleton: builder's account, spec-discharge table (issues 10/13 capabilities discharged; markdown floor contradicted in the UI), 12 findings; source of PR #10's backfilled record | +| [notes/deep-read-fe-1390](evidence/proofs/audits/deep-read-fe-1390.md) | active | FE-1401 / probes on FE-1419 | Deep-read of the capture store: spec-discharge table, write-time tiering assessment (penciled item 7), the FE-1405 status-arity answer, and live-probed confirmation of FE-1419's capture-store claims plus one new aliasing hole; source of PR #11's backfilled record | +| [plugin-contract-spec](specs/plugin-contract.md) | active | FE-1431 (spec issue); decided on FE-1405 | Provisional spec: a plugin is two schemas and two tables (model schema, proposal catalog, fold table, demand table) over the three-register IR (ADR-0003) β€” harness-machinery typology, standard-interiors library, grade-as-narrowing, derived fold rules; strains 4–7 and envelope pressure #2 held open with owners | + +## Control, architecture reference, and migration archive | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [COORDINATION](planning/_shared/COORDINATION.md) | active | cross-project; maintained by arc-close | Current sequencing recommendation, soft cross-map edges, unresolved seams, and exceptional roots; hard blockers, state, and hierarchy remain in Linear | -| [hash-monorepo-import-plan](planning/_shared/hash-monorepo-import-plan.md) | active until FE-1437 lands | FE-1437 | Native HASH assimilation plan: preserved history and child package workspaces under one Brunch context root, explicit authority cutover, exhaustive repository-material disposition, toolchain port, boundary gates, and verification | -| [SPEC-LEDGER](planning/_shared/SPEC-LEDGER.md) | active until milestone-one closure | FE-1383 | Obligation-level status and evidence ledger for the elicitation-kernel specification; settles when the milestone closes | -| [flue-architecture-cheatsheet](planning/_shared/flue-architecture-cheatsheet.md) | active | commented on FE-1383; feeds docs/agents/flue-routing.md | Architect's consolidation of all 21 Flue guide pages: direct structured generation uses `harness.prompt`; model-delegated work uses `useSubagent`; three-lane boundary summary and ranked divergence risks; reconciled against installed Flue 2.0.3 source | -| [topology](planning/_shared/topology.md) | active | ratified β†’ ADR-0002; N1 discharged by FE-1422 + FE-1392; local N5 implemented by FE-1391; N3 amended by FE-1437 | Pseudo-style verification of the package/app tree against the three-lane model and spec Β§12.2: portable ask/sweep protocols, Flue binding wiring, package boundaries, and application-only Brunch–Petrinaut composition | +| [hash-monorepo-import-plan](archive/migrations/hash-monorepo-import-plan.md) | settled | FE-1437 | Native HASH assimilation plan: preserved history and child package workspaces under one Brunch context root, explicit authority cutover, exhaustive repository-material disposition, toolchain port, boundary gates, and verification | +| [SPEC-LEDGER](control/SPEC-LEDGER.md) | active until milestone-one closure | FE-1383 | Obligation-level status and evidence ledger for the elicitation-kernel specification; settles when the milestone closes | +| [STEERING](control/STEERING.md) | active | FE-1357 / FE-1476 | One compact mutable authority for the current objective, proof frontier, soft edges, gates, beliefs, cuts, stop conditions, and exceptional roots | +| [STRATEGY-LOG](control/STRATEGY-LOG.md) | active append-only | governing IDs referenced by STEERING | Material strategic decisions and supersession history; distinct from accepted architecture ADRs and from status or diary history | +| [flue-architecture-cheatsheet](reference/architecture/flue-architecture-cheatsheet.md) | active | commented on FE-1383; feeds docs/agents/flue-routing.md | Architect's consolidation of all 21 Flue guide pages: direct structured generation uses `harness.prompt`; model-delegated work uses `useSubagent`; three-lane boundary summary and ranked divergence risks; reconciled against installed Flue 2.0.3 source | +| [topology](reference/architecture/topology.md) | active | ratified β†’ ADR-0002; N1 discharged by FE-1422 + FE-1392; local N5 implemented by FE-1391; N3 amended by FE-1437 | Pseudo-style verification of the package/app tree against the three-lane model and spec Β§12.2: portable ask/sweep protocols, Flue binding wiring, package boundaries, and application-only Brunch–Petrinaut composition | -## planning/legibility-sweep (FE-1401 arc records) +## Legibility engineering records and proofs (FE-1401) | Document | Status | Linear | Digest | | ------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [refactor-queue-2026-08-14](planning/legibility-sweep/refactor-queue-2026-08-14.md) | active | FE-1419 | Nine-commit refactor queue from an inductive review of open PR comments: capture-store contract closure + verification-oracle integrity; second-order review of the FE-1400 sweep's own countermeasures | -| [flue-patterns-audit-2026-08-17](planning/legibility-sweep/flue-patterns-audit-2026-08-17.md) | active | commented on FE-1383 | Audit of Flue usage against the official docs: substantially canonical; two fragile spots since fixed; its two "undocumented semantics" strain items were later resolved by the cheatsheet's agent-hooks read (documented after all β€” the pins stay) | -| [flue-entry-projection-source-read-2026-08-18](planning/legibility-sweep/flue-entry-projection-source-read-2026-08-18.md) | active | FE-1391 source gate + FE-1392 refresh oracle; reshapes FE-1386 | Installed Flue 2.0.3 evidence followed through to the public reader/archive and a causal refresh-before-apply oracle; FE-1386 remains one behavioral pin | -| [remediation-plan-2026-08-17](planning/legibility-sweep/remediation-plan-2026-08-17.md) | active | A1 discharged by FE-1422; A3/B1 by FE-1391; B2/B3 by source read + FE-1392 | Two ledgers from the consolidated sweep. FE-1392 resolves the private-model-call seam with direct `harness.prompt`, while keeping free-text/abandoned accounting in FE-1420 and the compaction pin in FE-1386 | -| [review-remediation-2026-08-18](planning/legibility-sweep/review-remediation-2026-08-18.md) | settled | FE-1432 | Executed queue from the cross-stack review: six lens-backed findings fixed, all 15 residual threads adjudicated and resolved, below-gate findings owned or refused, and three graduation proposals routed to FE-1401's tooling lane | -| [issue-pr-migration-2026-08-20/](planning/legibility-sweep/issue-pr-migration-2026-08-20/) | settled | FE-1451 | Completed legibility migration: byte-exact snapshots of 73 Linear issues and 25 GitHub PRs, reviewed proposals, canonical stored-target hashes, rollback data, a drift-gated validator, and an append-only apply log | +| [refactor-queue-2026-08-14](archive/engineering/legibility/refactor-queue-2026-08-14.md) | settled | FE-1419 | Nine-commit refactor queue from an inductive review of open PR comments: capture-store contract closure + verification-oracle integrity; second-order review of the FE-1400 sweep's own countermeasures | +| [flue-patterns-audit-2026-08-17](evidence/proofs/audits/flue-patterns-audit-2026-08-17.md) | active | commented on FE-1383 | Audit of Flue usage against the official docs: substantially canonical; two fragile spots since fixed; its two "undocumented semantics" strain items were later resolved by the cheatsheet's agent-hooks read (documented after all β€” the pins stay) | +| [flue-entry-projection-source-read-2026-08-18](evidence/proofs/audits/flue-entry-projection-source-read-2026-08-18.md) | active | FE-1391 source gate + FE-1392 refresh oracle; reshapes FE-1386 | Installed Flue 2.0.3 evidence followed through to the public reader/archive and a causal refresh-before-apply oracle; FE-1386 remains one behavioral pin | +| [remediation-plan-2026-08-17](archive/engineering/legibility/remediation-plan-2026-08-17.md) | settled | A1 discharged by FE-1422; A3/B1 by FE-1391; B2/B3 by source read + FE-1392 | Two ledgers from the consolidated sweep. FE-1392 resolves the private-model-call seam with direct `harness.prompt`, while keeping free-text/abandoned accounting in FE-1420 and the compaction pin in FE-1386 | +| [review-remediation-2026-08-18](archive/engineering/legibility/review-remediation-2026-08-18.md) | settled | FE-1432 | Executed queue from the cross-stack review: six lens-backed findings fixed, all 15 residual threads adjudicated and resolved, below-gate findings owned or refused, and three graduation proposals routed to FE-1401's tooling lane | +| [issue-pr-migration-2026-08-20/](archive/migrations/issue-pr-legibility-2026-08-20/) | settled | FE-1451 | Completed legibility migration: byte-exact snapshots of 73 Linear issues and 25 GitHub PRs, reviewed proposals, canonical stored-target hashes, rollback data, a drift-gated validator, and an append-only apply log | ## Decision records (`docs/adr/`) @@ -110,6 +117,6 @@ contract requires the spec to carry the new operating truth, in explicitly dated ## Path migration note (2026-08-12) -`.scratch/` is retired. Both efforts moved wholesale to `docs/planning//` (structure -preserved; relative links fixed). Linear references to `.scratch/...` paths predating this date -map 1:1 to `docs/planning/...`. +`.scratch/` is retired. Historical Linear references to `.scratch/...` and later +`docs/planning/...` locations retain their evidentiary meaning; current repository paths are the +role-based locations indexed above. diff --git a/libs/@hashintel/brunch-agent/docs/adr/0001-brunch-is-the-product-name.md b/libs/@hashintel/brunch-agent/docs/adr/0001-brunch-is-the-product-name.md index a12644b40a3..c47d08feb2d 100644 --- a/libs/@hashintel/brunch-agent/docs/adr/0001-brunch-is-the-product-name.md +++ b/libs/@hashintel/brunch-agent/docs/adr/0001-brunch-is-the-product-name.md @@ -3,7 +3,7 @@ Date: 2026-08-13 Status: accepted Amended: 2026-08-20 by ADR-0004 / FE-1437 (HASH package namespace) -Supersedes: spec [Β§12.3](../spec.md#123-naming--tool-namespacing) in part +Supersedes: spec [Β§12.3](../specs/elicitation-kernel.md#123-naming--tool-namespacing) in part Decided on: FE-1388 ## Context diff --git a/libs/@hashintel/brunch-agent/docs/adr/0002-topology-and-placement-rules.md b/libs/@hashintel/brunch-agent/docs/adr/0002-topology-and-placement-rules.md index b8a56713755..d32e82f59b3 100644 --- a/libs/@hashintel/brunch-agent/docs/adr/0002-topology-and-placement-rules.md +++ b/libs/@hashintel/brunch-agent/docs/adr/0002-topology-and-placement-rules.md @@ -3,17 +3,17 @@ Date: 2026-08-17 Status: accepted Amended: 2026-08-20 by ADR-0004 / FE-1437 (N3 application placement) -Refines: spec [Β§12.2](../spec.md) (package topology) with placement +Refines: spec [Β§12.2](../specs/elicitation-kernel.md) (package topology) with placement rules the spec did not state Decided on: FE-1401 (remediation sweep); ratified by Lu, 2026-08-17 ## Context The Flue architecture cheatsheet's full read of the framework's affordance surface -([flue-architecture-cheatsheet](../planning/_shared/flue-architecture-cheatsheet.md)) sorted this +([flue-architecture-cheatsheet](../reference/architecture/flue-architecture-cheatsheet.md)) sorted this system into three lanes: shell-facing affordances to consume directly, agent-loop capabilities to translate in the binding, and elicitation semantics plus the capture store to own outright. -The topology verification ([topology.md](../planning/_shared/topology.md)) then walked the actual tree +The topology verification ([topology.md](../reference/architecture/topology.md)) then walked the actual tree against that model and spec Β§12.2 and found one violation β€” the ask/suspension protocol's portable mechanism living in the Flue binding (the Β§14.2 second-binding test failing in spirit) β€” and no rule governing where upcoming work lands, which is how the violation happened. @@ -21,7 +21,7 @@ spirit) β€” and no rule governing where upcoming work lands, which is how the vi ## Decision The three-lane model and placement rules N1–N6, as specified in -[topology.md](../planning/_shared/topology.md), are ratified. In brief: +[topology.md](../reference/architecture/topology.md), are ratified. In brief: - **N1**: the ask protocol extracts to `core/src/ask-protocol.ts` (FE-1422, before FE-1392); sweep mechanism starts in `core/src/sweep-protocol.ts` from day one. Bindings contain hook diff --git a/libs/@hashintel/brunch-agent/docs/adr/0003-three-register-ir.md b/libs/@hashintel/brunch-agent/docs/adr/0003-three-register-ir.md index 41c926cc4b4..cb2dff3d678 100644 --- a/libs/@hashintel/brunch-agent/docs/adr/0003-three-register-ir.md +++ b/libs/@hashintel/brunch-agent/docs/adr/0003-three-register-ir.md @@ -2,7 +2,7 @@ Date: 2026-08-18 Status: accepted -Amends: [ir-design.md](../planning/process-model-elicitation/ir-design.md) Layer A (the +Amends: [ir-design.md](../specs/intermediate-representation.md) Layer A (the "Definition" paragraph), ratified FE-1364/FE-1397 Decided on: FE-1405 (payload-interiors session); ratified by Lu, 2026-08-18 diff --git a/libs/@hashintel/brunch-agent/docs/adr/0004-in-petrinaut-staging-and-the-monorepo-import.md b/libs/@hashintel/brunch-agent/docs/adr/0004-in-petrinaut-staging-and-the-monorepo-import.md index 990ded32c95..8472f336349 100644 --- a/libs/@hashintel/brunch-agent/docs/adr/0004-in-petrinaut-staging-and-the-monorepo-import.md +++ b/libs/@hashintel/brunch-agent/docs/adr/0004-in-petrinaut-staging-and-the-monorepo-import.md @@ -5,7 +5,7 @@ Status: accepted Amended: 2026-08-20 by FE-1437 (package family and imported application charter); 2026-08-21 by FE-1437 (Brunch context root) Supersedes: the demo-shell recommendation in -[recommendation-demo-vehicle](../planning/process-model-elicitation/recommendation-demo-vehicle.md) +[recommendation-demo-vehicle](../archive/decisions/superseded/recommendation-demo-vehicle.md) (FE-1362's resolution); amends ADR-0002's rule N3 Decided on: the 2026-08-18 integration meeting (Dei, Chris, Lu); recorded on FE-1433 @@ -70,7 +70,7 @@ elicitor is staged, not what it emits. this ADR; FE-1331 (start elicitation from create-new-net) is un-deferred β€” in-Petrinaut initiation is now the September topology, not the post-September one. - The integration build was specified on FE-1433 - ([petrinaut-integration-spec](../planning/process-model-elicitation/petrinaut-integration-spec.md)), + ([petrinaut-integration-spec](../specs/petrinaut-integration.md)), and both gating spikes reported: Flue turn suspension carries client-tool round-trips, and the Pi-to-AI-SDK stream adapter drives Petrinaut's panel. - The review stack and spikes have landed. FE-1437 records the final standalone SHA and imports diff --git a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md index 5f24239ee15..cec8e534cbc 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md +++ b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md @@ -18,51 +18,58 @@ the history of these control surfaces. Always inspect `docs/inbox/` and `docs/INDEX.md` together. -- Move settled inbox material to its canonical home under `docs/reference/`, - `docs/planning//`, or `docs/planning/_shared/`. +- Promote settled inbox material to its role-based home under `documentation.md`. Do not add to the + retired `docs/planning/` or `docs/history/` paths. - Delete source material only when its durable information has been transferred and its consumption is named. -- Ensure every non-archive Markdown document is indexed and every index entry still resolves. +- Ensure every document under `docs/` except `docs/INDEX.md` and `docs/agents/**` is indexed and + every index entry still resolves. - Update status, ownership, and digest text when this arc changed their truth. ### 2. Audit the Linear registry and touched references -Always run `turbo run linear:graph --filter '@hashintel/brunch-agent'` and inspect every open project -issue with no parent. +Always run `turbo run linear:graph --filter '@hashintel/brunch-agent' -- --all`. Inspect every +project issue's assignment and every open issue with no parent. - Every non-root issue must have a parent. - Every intentional root must be a recognized map or sweep root under the registry rule, or be - named under **Exceptional roots** in `docs/planning/_shared/COORDINATION.md`. -- Repair missing parentage in Linear when the intended owner is unambiguous; otherwise record the - unresolved root in `COORDINATION.md`. + named under **Exceptional roots** in `docs/control/STEERING.md`. +- Subject to [the issue tracker's external-write approval gate](issue-tracker.md#conventions), + repair missing parentage in Linear when the intended owner is unambiguous; otherwise record the + unresolved root in `STEERING.md`. - Re-read every issue the arc closes or materially changes. Repair stale branch, dependency, - evidence, and document references before closing it. + evidence, and document references before closing it, subject to the same approval gate for any + Linear mutation. - Follow `issue-writing.md`: preserve a root issue's human-owned contract and put agent-maintained detail inside `πŸ—οΈ Agent notes`. ### 3. Reconcile the spec ledger when affected -Update `docs/planning/_shared/SPEC-LEDGER.md` in the same change when the arc builds, disproves, +Update `docs/control/SPEC-LEDGER.md` in the same change when the arc builds, disproves, supersedes, or changes evidence for a milestone-one specification obligation. Change the smallest affected set of rows; do not add an evaluation narrative. When milestone one closes, settle the ledger as a terminal record rather than keeping it artificially live. -### 4. Reassess project coordination when affected +### 4. Reconcile the current control when affected -Reassess `docs/planning/_shared/COORDINATION.md` when the arc changes: +Reassess `docs/control/STEERING.md` only when exceptional roots, active soft edges, or project-wide +sequencing/strategy materially change. Ordinary blockers, parents, project membership, and ticket +movement remain Linear facts and are not independent steering triggers. -- a hard blocker, issue parent, project membership, or exceptional root; -- a soft `coord`, `input`, or `state-gate` edge; -- an unresolved cross-map seam; or -- issue semantics that could change the current project-wide sequencing recommendation. +Use the Linear graph for deterministic facts, then check soft edges, exceptional roots, governing +strategy, gates, and proof frontier. Linear remains canonical for hard blockers, state, assignment, +and hierarchy. If the judgment did not change, leave the control untouched. If a material strategy +trigger fired, validate that the new append-only strategy entry has evidence and supersedes, and +that `STEERING` references it. No-op reconciliation persists nothing. -Use `turbo run linear:graph --filter '@hashintel/brunch-agent'` for deterministic facts, then read -the relevant issue bodies and infer the recommendation. Linear remains canonical for hard blockers, -state, and hierarchy. Keep one compact pseudo-style map of the current recommendation; do not paste -the full generated graph or maintain alternative or historical orderings. If the judgment did not -change, leave the document untouched. +### 5. Reconcile steering and proof when triggered -### 5. Repair tense and report +If a steering trigger fired, run and reconcile [the steering protocol](steering.md), including its +completion criteria. Do not copy its loop here. + +If no trigger fired, continue the current proof frontier without a no-op steering update. + +### 6. Repair tense and report Read the arc's planning prose in the state that will exist after landing. Remove stale future tense, provisional labels, temporary pointers, and inaccurate status language. Report which @@ -77,5 +84,6 @@ Arc close is complete when: 2. the Linear orphan audit has no unexplained roots; 3. touched issue references are current; 4. affected spec-ledger rows are current; -5. affected coordination judgment is current; and -6. changed planning prose reads correctly after landing. +5. affected steering soft edges, roots, strategy, gates, and frontier are current; and +6. any triggered steering pass meets `steering.md`'s completion criteria; and +7. changed planning prose reads correctly after landing. diff --git a/libs/@hashintel/brunch-agent/docs/agents/documentation.md b/libs/@hashintel/brunch-agent/docs/agents/documentation.md index daa7b5a05ff..c93786f97e5 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/documentation.md +++ b/libs/@hashintel/brunch-agent/docs/agents/documentation.md @@ -1,71 +1,90 @@ -# Documentation protocol: ingest, settle, index - -How documents enter the Brunch context, where they end up, and how we prove nothing is lost. Companion -to `issue-tracker.md` (which governs issues; this file governs documents). - -## Zones - -| Zone | Role | Lifetime | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -| `docs/inbox/` | Untriaged arrivals: PDFs, exports, transcripts, pasted research. Timestamped filenames where the arrival date matters. | Temporary β€” everything here is awaiting settlement | -| `docs/planning//` | All artifacts of an effort, active or complete: notes, research findings, maps, specs β€” nested by effort name. | Permanent (dispositioned at effort close) | -| `docs/planning/_shared/` | Cross-effort control documents: current coordination, obligation ledgers, topology, and consolidations that outlive any one effort. | Permanent; lifecycle declared by each document | -| `docs/reference/` | Settled documents of lasting value: external reports, transcripts, digested research. | Permanent | -| `docs/INDEX.md` | The TOC: one line per settled or in-flight document β€” title, date, one-line digest, provenance, where used. | Permanent, always current | -| `CONTEXT.md` | Glossary only (see domain-modeling discipline). | Permanent | - -**Ephemera** live outside all zones: any directory named `drafts/` is git-ignored (root -`.gitignore`), for documents whose delivered form is the record β€” outbound comment and message -drafts, one-off prep. Place one directly inside the effort it serves -(`docs/planning//drafts/` β€” no deeper nesting). Never register drafts in `INDEX.md`, and never link a -`drafts/` path from Linear or a committed document β€” once delivered, link the destination -(the posted comment, the sent message) instead. - -**Control surface vs record.** A _record_ is arc-scoped, carries its date in the filename, and -eventually stops changing β€” it lives inside the effort directory that produced it. A -cross-effort _control surface_ is never dated in the filename and lives in -`docs/planning/_shared/`. Most remain current; a bounded ledger may declare its own terminal -condition and settle when that condition is met. The planning top level contains only -directories; this rule and `INDEX.md` coverage (every file has a row, every row's path resolves) -are enforced by `packages/core/test/architecture/docs-index.test.ts`. Freshness β€” whether statuses and digests are still -_true_ β€” cannot be mechanized without building a dead gate; it belongs to the arc-close sweep. - -External stores (Linear, Notion) hold _pointers and mirrors_, never the only copy: Linear issue -descriptions gist and link; Notion pages that originate content (e.g. team-facing question docs) -are listed in `INDEX.md` with their URL. - -## The ingest protocol - -1. **Arrive**: new material lands in `docs/inbox/` (timestamped name if the date matters). -2. **Register**: the first time a document is _used_ (read into an effort, cited by a ticket), - add its line to `docs/INDEX.md` with status `inbox`. -3. **Settle**: move it to its permanent home (`docs/reference/` for external/source material; - `docs/planning//` if it is a working artifact of that effort), update its `INDEX.md` - line (new path, status `settled`), and fix any links that pointed at the inbox path. -4. **Sweep**: at every effort boundary (map charted, map closed), empty the inbox β€” everything - either settles or is deleted _with its INDEX line recording the deletion and reason_. This - runs as step 1 of the arc-close sweep (`arc-close.md`). - -## Referencing ephemera (issues, external state) - -Long-lived documents outlive the trackers they cite. Three rules: - -- **Gloss at first mention**: an issue ID in a living document is introduced with its gist β€” - "FE-1423 (the pre-remote gates)" β€” so the document degrades gracefully for a reader who - cannot resolve Linear. -- **Load-bearing only in the tracking layer**: coordination and obligation/remediation ledgers - may depend on issue resolution β€” tracking is their job. Everywhere else an issue ID is a - parenthetical citation the sentence must survive without. -- **Tense repair at arc close**: prophecy becomes history when the issue lands ("will - extract" β†’ "extracted (FE-1422)") β€” step 5 of `arc-close.md`. - -## Effort completion - -When an effort closes, its `docs/planning//` directory is reviewed file-by-file and -the review recorded in `INDEX.md`: tracker records (map, tickets) are mirrored to Linear if -not already there; everything else stays in place with status `settled`. (`.scratch/` was -retired 2026-08-12 β€” efforts live under `docs/planning/` from birth.) - -**Nothing is deleted until its `INDEX.md` line records the disposition.** Linear mirrors gist -and link; the repo copy is canonical, so a repo path referenced from Linear must never be -deleted without updating the Linear reference. +# Documentation protocol: ingest, promote, index + +This protocol assigns one authority to each kind of durable knowledge. It complements +`issue-tracker.md`, which governs issue facts. `docs/agents/` remains the operating-guidance set, +and `docs/INDEX.md` remains the registry described below. + +## Authoritative topology + +| Location | Role | +| --- | --- | +| `docs/control/` | Current steering, append-only strategy rationale, and the separate obligation ledger | +| `docs/specs/` | Required behavior | +| `docs/adr/` | Accepted decisions | +| `docs/evidence/proofs/` | Immutable observed proof, witness, and implementation snapshots | +| `docs/evidence/evaluations/` | Immutable evaluation runs and readouts | +| `docs/reference/` | Stable explanatory and source material | +| `docs/archive/` | Historical, superseded, or settled material with no current authority | +| `docs/inbox/` | Transient, untriaged intake | +| `evaluations/cases/` | Executable evaluation cases and corpora | +| `evaluations/protocols/` | Executable evaluation procedures | +| `evaluations/oracles/` | Executable checks for validated categorical claims | + +Effort is metadata recorded in the index or document, not a placement rule. Do not create an +effort-shaped documentation tree. + +The role topology is physically authoritative. No document may remain under the retired +`docs/planning/` or `docs/history/` paths. + +External stores hold pointers or tracker facts, never the only repo-owned document. Before moving +or deleting a path referenced from Linear, Notion, or another external store, repair every pointer; +external writes require their own approval. Nothing is deleted until its `INDEX.md` entry records +the disposition. + +## Evidence vocabulary + +- **Case / corpus**: bounded evaluation input, or a reviewed collection from which cases are + selected. It defines what may be presented, not the expected answer. +- **Fixture**: reviewed, versioned setup derived from a case and suitable for a production-path + run. It may provide domain state, never missing product wiring. +- **Run snapshot**: immutable capture of inputs, environment identity, production entrypoint, + outputs, and result for one execution. +- **Oracle**: executable check promoted from a validated categorical claim. Hidden answer keys and + oracles stay behind the information wall and are not interviewee or elicitor inputs. + +An **immutable legibility snapshot** renders the proof in another register and records the strain +found during translation. A **witness record** identifies the claim observed, scenario and build, +witness, date, observation, and verdict. UX, interpretation, live-runtime, and +demo-comprehension claims require one unless explicitly inapplicable. See `legibility.md` for the +render-and-read mechanics. + +## Intake, promotion, and disposition + +1. **Arrive:** transient material enters `docs/inbox/`, timestamped when arrival time matters. +2. **Register:** when first used, add it to `docs/INDEX.md` with status `inbox`. +3. **Review:** identify provenance, consumer, information-wall boundary, and intended authority. +4. **Promote:** + - required behavior goes to `docs/specs/`; + - accepted decisions go to `docs/adr/`; + - observed proof and witness snapshots go to `docs/evidence/proofs/`; + - evaluation run readouts go to `docs/evidence/evaluations/`; + - stable explanations and source material go to `docs/reference/`; + - executable cases, protocols, and oracles go outside docs under `evaluations/`; + - superseded or settled context with no present authority goes to `docs/archive/`. +5. **Reconcile:** update the index and all internal and approved external pointers atomically. + +Promote a validated categorical claim to an oracle when recurrence is plausible, the claim is +mechanizable, and silent regression matters. Keep judgment that cannot be mechanized in protocol +or review guidance. A run snapshot never becomes a mutable control, and a control surface never +accumulates run history. + +## Mutable controls + +`STEERING.md` is the one compact mutable strategic control: current objective, proof frontier, soft +edges, choices, gates, beliefs, exceptional roots, and stop conditions. `STRATEGY-LOG.md` is +immutable append-only rationale for material strategic choices; it is distinct from ADRs, which own +accepted architecture. `SPEC-LEDGER.md` remains a separate conditional obligation control. Link +evidence and history instead of copying chronology: Git is the mutable-control history. Do not add +diary or status entries to either strategic control. Linear owns issue state, hierarchy, assignment, +and hard blockers. + +## Index and link rules + +- Every document under `docs/` except `docs/INDEX.md` and `docs/agents/**` has exactly one covering + `docs/INDEX.md` row, and every row resolves. Every agent protocol is reachable from `AGENTS.md`. +- Introduce an issue ID with a gist so prose survives loss of tracker access. Outside control and + tracking surfaces, issue IDs are citations rather than load-bearing facts. +- Reconcile stale tense, status, provenance, and links when an arc changes their truth. +- Drafts are git-ignored ephemera, never indexed or linked from durable records. + +Arc close performs inbox, index, pointer, and control reconciliation; see `arc-close.md`. diff --git a/libs/@hashintel/brunch-agent/docs/agents/flue-routing.md b/libs/@hashintel/brunch-agent/docs/agents/flue-routing.md index 62938e2db1e..6184e885071 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/flue-routing.md +++ b/libs/@hashintel/brunch-agent/docs/agents/flue-routing.md @@ -4,8 +4,8 @@ Consult this at design moments β€” when you notice yourself about to add state, loop, a route, or a test harness β€” _before_ writing the new thing. Each row routes an indication to the affordance to rely on, the divergence it exists to prevent, and the point where canon stops and a human or an owning ticket decides. Every row is grounded in the -[architecture cheatsheet](../planning/_shared/flue-architecture-cheatsheet.md) (Β§ refs), the -[patterns audit](../planning/legibility-sweep/flue-patterns-audit-2026-08-17.md), or the +[architecture cheatsheet](../reference/architecture/flue-architecture-cheatsheet.md) (Β§ refs), the +[patterns audit](../evidence/proofs/audits/flue-patterns-audit-2026-08-17.md), or the [flue-vs-tilde analysis](../reference/amp-analysis-flue-vs-tilde.md); details live there. **Which lane am I in?** Flue's surface sorts our system into three lanes (cheatsheet, diff --git a/libs/@hashintel/brunch-agent/docs/agents/git-workflow.md b/libs/@hashintel/brunch-agent/docs/agents/git-workflow.md index 90600c48944..759fd26bf8d 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/git-workflow.md +++ b/libs/@hashintel/brunch-agent/docs/agents/git-workflow.md @@ -16,6 +16,14 @@ Use **git** for local operations that don't touch the stack: `status` / `diff` / stack parentage metadata; commits and reads are safe as plain git (run `gt restack` afterwards if upstack branches exist). Do not use `gh stack` in `hashintel/hash`. +## Shared-worktree safety + +A branch switch changes the checkout under every process using that worktree. Before `gt create` +or `gt checkout`, record the current branch and inspect the worktree for unexpected changes or +in-flight agents. If another agent may be active, use a separate worktree instead of switching the +shared one; never stash, reset, clean, or otherwise claim work you did not create. Restore the +branch you found after the stack operation unless the user asks to leave the worktree elsewhere. + ## Naming - **Branch**: `{prefix}/{issue-id}-{keywords}` (e.g. `ln/fe-1362-demo-vehicle`). diff --git a/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md index 60d329f0c94..757005554e7 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md +++ b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md @@ -2,8 +2,9 @@ Issues for this repo live in **Linear**, worked through the `linear` CLI (see the `tool-linear-cli` skill). Long-form artifacts β€” specs, research write-ups, notes β€” stay in this -repo under `docs/planning//` (see `documentation.md`); Linear issues link to them by repo path (and to Notion/Google -docs by URL). The issue is the tracker record; the repo file is the document. +repo in the role-based topology defined by `documentation.md`; Linear issues link to them by repo +path (and to Notion/Google docs by URL). The issue is the tracker record; the repo file is the +document. How issue titles and bodies are written β€” the contract/execution-record split, outcome-shaped titles, plain-prose context β€” is covered in `issue-writing.md`; it applies to every issue an @@ -11,8 +12,14 @@ agent authors from this repo. ## Conventions -- **Default team**: `FE`. **Project**: `brunch-agent`. Create issues with - `linear issue create --team FE --project brunch-agent`. The project is the +**Approval gate:** reading Linear is allowed, but obtain explicit approval before any issue +creation, edit, comment, hierarchy change, state change, project mutation, or other write. Approval +for one named operation does not authorize adjacent mutations. + +- **Default team**: `FE`. **Project**: `brunch-agent`. Every approved new issue is created with + `linear issue create --team FE --project brunch-agent --assignee self`, plus `--parent` when it + is not a root. Verify assignee, project, and parent after creation. Assignment denotes accountable + human ownership, never an agent claim. The project is the ownership boundary for this codebase β€” see the registry rule below. - Related work also lives on teams `PRO` (product) and `H` (HASH) β€” read/reference those freely; create there only when asked. The legacy `brunch` project holds the old brunch product's @@ -26,8 +33,9 @@ agent authors from this repo. ## When a skill says "publish to the issue tracker" -Create a Linear issue on team `FE`, project `brunch-agent`. If the content is long-form, commit -it under `docs/planning//` and link the repo path from the issue description. +After explicit approval, create a Linear issue on team `FE`, project `brunch-agent`. If the content +is long-form, place it by its role under `documentation.md` and link the repo path from the issue +description. ## When a skill says "fetch the relevant ticket" @@ -67,7 +75,9 @@ update. Publish it to the `brunch-agent` project. ## Wayfinding operations Used by the `ds-wayfind` skill. The **map** is a Linear issue with one **child** sub-issue per -ticket. +ticket. Linear owns issue facts: state, hierarchy, project membership, assignment, and hard +blockers. `STEERING` projects only current soft edges and chooses strategic work by objective +contribution, risk retired, and information gain. - **Map**: an FE issue in project `brunch-agent`, labeled `wayfinder β†’ map` (label group `wayfinder`, children `map` / `research` / `prototype` / `grilling` / `manual-task` β€” created 2026-08-11; @@ -78,9 +88,9 @@ ticket. carrying its `wayfinder β†’ ` label. The description holds the question. - **Blocking**: Linear's native **blocks / blocked-by** relations. A ticket is unblocked when every issue blocking it is closed (Done or Canceled). -- **Frontier**: open, unblocked, **unassigned** sub-issues of the map β€” lowest issue number - first. -- **Claim**: assign the issue to yourself (the dev driving the map) before any work. +- **Mechanical frontier**: open, unblocked sub-issues of the map β€” lowest issue number first. This + is an availability filter, not the strategic proof frontier. Before work, inspect issue state and + any active branch or PR to avoid duplicate execution. - **Resolve**: post the answer as a comment on the issue, set state **Done**, then append a one-line gist + link to the map issue's _Decisions so far_ section (edit the map description). - **Out of scope**: set state **Canceled** and record the gist + reason in the map's @@ -97,28 +107,28 @@ in the `brunch-agent` project. Nothing else β€” no label, no team, no root-walki belonging. Within the project, every issue must additionally be **reachable from a root**: either it is a sub-issue (directly or transitively) of a root map β€” currently FE-1383 (build) and FE-1357 (demo + plugin spec) β€” or a sub-issue of a named sweep ticket (FE-1401-style), or it -_is_ a root and `docs/planning/_shared/COORDINATION.md` names it under **Exceptional roots**. An +_is_ a root and `docs/control/STEERING.md` names it under **Exceptional roots**. An issue in the project but reachable from no root is captured-then-orphaned, the failure mode this rule exists to stop. Set the parent at creation (`--parent FE-XXXX`), not in a later sweep. Audit (run at arc close, alongside the legibility protocol's consolidation step): ```shell -turbo run linear:graph --filter '@hashintel/brunch-agent' +turbo run linear:graph --filter '@hashintel/brunch-agent' -- --all ``` Check each open row without a `p:` parent against the roots above. An orphan gets a parent or an -explicit root listing in `COORDINATION.md` β€” silence is not an option it has. The projection is -project-wide rather than assignment-scoped: assignment is fallible, and more contributors means -unassigned work is normal. A shared custom view β€” "brunch-agent: open without parent" β€” surfaces +explicit root listing in `STEERING.md` β€” silence is not an option it has. The projection is +project-wide rather than assignment-scoped. The graph also audits every project issue against the +current authenticated user; mismatches require review, never auto-repair. Existing reassignment is +a separate write and remains approval-gated. A shared custom view β€” "brunch-agent: open without parent" β€” surfaces candidate orphans continuously; its only legitimate rows are the roots themselves. ## Historical note Before 2026-08-11 this repo tracked issues as local markdown under `.scratch//` -(map at `map.md`, tickets at `issues/NN-.md`; live trees moved under `docs/planning/`, -completed ones under `docs/history/planning/`). The completed `elicitation-kernel` effort -remains in that form as the canonical archive (now at `docs/history/planning/elicitation-kernel/`), and is **mirrored in Linear for team +(map at `map.md`, tickets at `issues/NN-.md`). The completed `elicitation-kernel` effort +remains in that form as the canonical archive (now at `docs/archive/elicitation-kernel/`), and is **mirrored in Linear for team visibility** as FE-1366 (map) with sub-issues FE-1367–FE-1379, all Done, blocking relations preserved. New efforts go to Linear directly. diff --git a/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md b/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md index 8f24631c13c..c93902b9509 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md +++ b/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md @@ -12,6 +12,12 @@ formality: complete sentences, short paragraphs, active voice, concrete nouns an State causal, temporal, and conditional relationships explicitly. Keep sentence structure simple without simplifying the idea; prefer two clear sentences over one compressed sentence. +Respect the reader's existing knowledge. Supply the context needed to judge the change, but do +not add named theories, tutorial passages, or persuasive flourishes merely to demonstrate +authority. Walk through the relevant evidence, state the verdict at the strength that evidence +supports, and label opinion as opinion. Trust colleagues to ask when they need more; cut +pre-emptive explanation before cutting facts they need to act. + Use shared product and system vocabulary, but avoid implementation-specific names unless they are necessary to explain the behavior. Do not write in telegraphic, checklist, ticket, changelog, or runbook style. Avoid fragments and labeled fields. Use a list only when readers @@ -173,7 +179,7 @@ issue closes β€” a closing act, not a workspace. State a fact once; everywhere else links. Within an issue, a plan links to a decision, never restates it. Across issues, a sub-issue never re-explains a fact its parent's context already -states. Long-form artifacts live in the repo (`docs/planning//`) and are linked by +states. Long-form artifacts live in the repo's role-based documentation zones and are linked by path, per `issue-tracker.md`. ## Voice and authority diff --git a/libs/@hashintel/brunch-agent/docs/agents/legibility.md b/libs/@hashintel/brunch-agent/docs/agents/legibility.md index 20d5a462492..c46aae550e4 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/legibility.md +++ b/libs/@hashintel/brunch-agent/docs/agents/legibility.md @@ -46,6 +46,18 @@ pass over the filings before the arc closes. The FE-1405/FE-1406 round came from first round's own text ("shapes-to-fill" quoted back); the gaps were real and had been deepened by the filings meant to close them. +## Point findings may reveal a recurrent class + +A point finding is evidence of a possible fault class, not proof that the whole codebase shares +it. Promote the finding to an audit only when recurrence is plausible, the class is cheaply +searchable, and missed instances could fail silently. Search both the mechanical family and the +ownership boundaries where the same contract may be reimplemented. Keep finding and fixing as +separate steps so a human can confirm the class and scope before repair begins. + +When the class is confirmed, make its categorical part executable as a boundary or regression +check. Put any judgment that cannot be mechanized into review guidance. Do not widen a one-off +finding into a repository sweep merely for symmetry. + ## Consolidation: capture-as-we-go, reconcile-before-landing Capture channels (accrual comments, pencil lists, strain appendices, handoffs) guard against @@ -55,7 +67,7 @@ evaporation, not fragmentation. Two rules keep the yield coherent: owning control surface, pencils graduate to issues or planning documents, and strain reports become document fixes. A channel with no named target is a leak with a delay. - **An arc is not closed until consolidation runs.** The closing step reconciles what the - captures established into the durable artifacts (coordination, ledger, docs, issues) β€” a + captures established into the durable artifacts (steering/`STEERING`, ledger, docs, issues) β€” a handoff note alone is a deferral, not a deposit. ## Deposit: work describes itself at authoring time diff --git a/libs/@hashintel/brunch-agent/docs/agents/steering.md b/libs/@hashintel/brunch-agent/docs/agents/steering.md new file mode 100644 index 00000000000..cf3faf4d19a --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/agents/steering.md @@ -0,0 +1,107 @@ +# Strategic steering: orient, choose, execute, reconcile, replan + +Use this protocol to keep Brunch aimed at a current, falsifiable proof rather than at whatever +issue happens to be available. The arc driver owns trigger evaluation. Current truth lives in +`STEERING`, which references governing `STRATEGY-LOG` IDs; this document owns only the control loop. + +Run a steering pass when: + +- work starts or resumes without a current proof target; +- the objective, deadline, use case, or pressure changes; +- a proof succeeds or fails; +- authorities conflict; +- an external gate changes or becomes stale; +- the selected frontier loses value; or +- arc close detects strategic drift. + +Ordinary ticket movement is not a steering trigger. + +## Orient + +Classify every load-bearing input as one of: + +- **fact**; +- **belief** with confidence and cited evidence; +- **unknown** with its cheapest probe; or +- **external gate** with owner, source, watch trigger, last-checked date, and consequences. + +Do not turn confidence into fact. A confidence change cites the evidence that changed it. + +## Choose + +Treat the mechanical frontier (open, unblocked issues) as a filter, not a priority rule. Rank +eligible moves by objective contribution, risk retired, information gain, +deadline pressure, and cost/reversibility. + +Select one proof frontier, or a deliberate pair whose join is named. Record: + +- **claim** β€” the proposition this frontier can validate; +- **proof bundle** β€” the fields below; +- **cut** β€” work explicitly excluded before the proof; +- **issue/gap projection** β€” existing issues and uncovered work needed to execute it; and +- **stop/replan trigger** β€” the observation that ends or redirects the attempt. + +## Execute + +Exercise real production entrypoints and wiring. A fixture may supply domain inputs, but it must +not supply product wiring absent from the product. Require both a runnable proof and an immutable +legibility snapshot in another register. UX, interpretation, live-runtime, and demo-comprehension +claims require a human witness unless the claim explicitly records why witness is inapplicable. + +The evidence lifecycle is: + +```text +corpus/case -> reviewed fixture -> production-path run -> immutable run snapshot + -> validated claim -> executable oracle +``` + +Maintain the information wall: hidden answer keys and oracles are evaluation-side material, never +inputs to the interviewee or elicitor under evaluation. + +### Proof-bundle fields + +- claim and bounded scenario; +- production entrypoint and wiring exercised; +- reviewed fixture and provenance; +- runnable command or procedure and result; +- immutable run snapshot path; +- legibility snapshot path and register; +- witness record, or explicit inapplicability; +- observed failures and residual uncertainty; +- validated, rejected, or narrowed claim; +- oracle candidate and promotion decision. + +## Reconcile + +Deposit each changed truth into exactly one authority and link to it elsewhere: + +| Truth | Authority | +| --- | --- | +| Current objective, proof frontier, soft edges, cuts, beliefs, gates, and exceptional roots | `STEERING` | +| Material strategic decision rationale | append-only `STRATEGY-LOG` | +| Issue state, hierarchy, and hard blockers | Linear | +| Required behavior | `docs/specs/` | +| Accepted decisions | `docs/adr/` | +| Observed proof and witness snapshots | `docs/evidence/proofs/` | +| Evaluation runs and readouts | `docs/evidence/evaluations/` | +| Stable explanation and source material | `docs/reference/` | +| Superseded or settled historical context | `docs/archive/` | + +Linear writes require explicit approval before creation or mutation. Link; do not copy history or +evidence into mutable controls. Follow [documentation](documentation.md) for placement and +promotion, [legibility](legibility.md) for the second-register read, +[issue-tracker](issue-tracker.md) for tracker mechanics, and [arc-close](arc-close.md) for the +closing control pass. + +## Replan + +If a trigger fired, return to orient and choose. Otherwise continue the selected frontier; do not +replan merely because ticket state moved. + +Append IDs monotonically. A conflicting decision names the governing ID it supersedes; a +complementary decision uses `none`. Only unsuperseded governing IDs remain in `STEERING`. A no-op +writes nothing. + +The pass is complete when inputs are classified, one frontier (or named pair) has all choice +fields, its proof bundle is runnable and indexed, every changed truth has exactly one authority, +confidence changes cite evidence, and the next stop/replan trigger is explicit. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/recommendation-demo-vehicle.md b/libs/@hashintel/brunch-agent/docs/archive/decisions/superseded/recommendation-demo-vehicle.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/recommendation-demo-vehicle.md rename to libs/@hashintel/brunch-agent/docs/archive/decisions/superseded/recommendation-demo-vehicle.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/01-flue-architecture-deep-read.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/01-flue-architecture-deep-read.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/01-flue-architecture-deep-read.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/01-flue-architecture-deep-read.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/02-zil-lean-survey.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/02-zil-lean-survey.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/02-zil-lean-survey.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/02-zil-lean-survey.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/03-brunch-exchange-schema-audit.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/03-brunch-exchange-schema-audit.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/03-brunch-exchange-schema-audit.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/03-brunch-exchange-schema-audit.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/04-contract-decomposition.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/04-contract-decomposition.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/04-contract-decomposition.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/04-contract-decomposition.md index 66002ac067e..9713280f1d3 100644 --- a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/04-contract-decomposition.md +++ b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/04-contract-decomposition.md @@ -25,7 +25,7 @@ Concrete test cases for every boundary claim (from issue 07): how would `elicit- ## Answer -> Resolved by HITL grilling, 2026-08-06 (four rounds, including an evidence pass over `~/Clones/mattpocock/skills` + `../brunch/docs/design/BEHAVIORAL_KERNELS.md`, and integration of [agentic-elicitation-criteria](../../../../reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md), the second inbox doc). +> Resolved by HITL grilling, 2026-08-06 (four rounds, including an evidence pass over `~/Clones/mattpocock/skills` + `../brunch/docs/design/BEHAVIORAL_KERNELS.md`, and integration of [agentic-elicitation-criteria](../../../reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md), the second inbox doc). ### Ownership table diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/05-questioning-ux-contract.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/05-questioning-ux-contract.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/05-questioning-ux-contract.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/05-questioning-ux-contract.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/06-shipping-shape.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/06-shipping-shape.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/06-shipping-shape.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/06-shipping-shape.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/07-dev-target-portfolio.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/07-dev-target-portfolio.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/07-dev-target-portfolio.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/07-dev-target-portfolio.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/08-assemble-the-spec.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/08-assemble-the-spec.md similarity index 91% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/08-assemble-the-spec.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/08-assemble-the-spec.md index 4c062fd66ba..1d228983b42 100644 --- a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/08-assemble-the-spec.md +++ b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/08-assemble-the-spec.md @@ -13,7 +13,7 @@ Assemble the destination spec from the resolved decisions: architecture + contra > Resolved by spec assembly, 2026-08-10. Full read of the ~54K-token working set (map, glossary, tickets 01–13 in the pre-pass's amendment order, both inbox docs), one session, no digest subagents β€” as sized. -**The spec is assembled: [spec.md](../../../../spec.md)** β€” the map's terminal deliverable. Fourteen sections + an adjudications appendix: purpose Β· non-goals & the elicitorβ†’executor seam Β· vocabulary Β· four shells + binding Β· the capture envelope (derived-status principle, three strata) Β· operations & validation strata Β· questioning-UX contract Β· capture mechanics Β· sessions/durability/storage port Β· the ten-item substrate-capability list Β· plugins & packs Β· shipping shape Β· dev targets & milestone one (gherkin + assurance argument, `Statement` contract) Β· acceptance material (five proof obligations, ten harness invariants restated in envelope vocabulary, gating tests, testing strategy, open verification items). +**The spec is assembled: [spec.md](../../../specs/elicitation-kernel.md)** β€” the map's terminal deliverable. Fourteen sections + an adjudications appendix: purpose Β· non-goals & the elicitorβ†’executor seam Β· vocabulary Β· four shells + binding Β· the capture envelope (derived-status principle, three strata) Β· operations & validation strata Β· questioning-UX contract Β· capture mechanics Β· sessions/durability/storage port Β· the ten-item substrate-capability list Β· plugins & packs Β· shipping shape Β· dev targets & milestone one (gherkin + assurance argument, `Statement` contract) Β· acceptance material (five proof obligations, ten harness invariants restated in envelope vocabulary, gating tests, testing strategy, open verification items). **All seven pre-pass contradictions adjudicated** (spec Appendix A): C1 storage port binding-implemented, with the `db.ts`-vs-capture-store reconciliation stated; C2 the four operations stay pure β€” ticket 12's PluginContext-storage clause scoped to non-op code, milestone one defines no such methods; C3 status derived, never stored β€” retraction specified as an explicit user-cited event with no successor; C4 tap-ness made a transport fact via a harness-defined reserved reply encoding (else absences are inferred); C5 invariant 1 reconciled with the provenance rule β€” user-derived captures cite user entries, `defaulted`/`external-lookup` cite declared defaults/documented transformations; C6 the channel is a per-message current-affordance surface, durable identity on tool output parts, reject-second-interactive-affordance as mechanism; C7 picked together β€” no instruction interpolation (kills the wake wart's cause), pending question on the ask tool result + pending-affordance slot, reply binding harness-mechanical via the single-pending invariant, no echo token. diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/09-formal-verification-canon-survey.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/09-formal-verification-canon-survey.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/09-formal-verification-canon-survey.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/09-formal-verification-canon-survey.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/11-logic-prototype-capture-sweep.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/11-logic-prototype-capture-sweep.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/11-logic-prototype-capture-sweep.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/11-logic-prototype-capture-sweep.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/12-multi-session-durable-target.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/12-multi-session-durable-target.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/12-multi-session-durable-target.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/12-multi-session-durable-target.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/13-walking-skeleton-sweep-seam.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/13-walking-skeleton-sweep-seam.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/issues/13-walking-skeleton-sweep-seam.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/issues/13-walking-skeleton-sweep-seam.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/map.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/map.md similarity index 93% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/map.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/map.md index 2b71cae130f..788c2a70371 100644 --- a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/map.md +++ b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/map.md @@ -2,7 +2,7 @@ Label: wayfinder:map Status: closed β€” destination reached 2026-08-10 (the spec is assembled: -[spec.md](../../../spec.md)) +[spec.md](../../specs/elicitation-kernel.md)) Created: 2026-08-06 ## Destination @@ -16,7 +16,7 @@ A reviewable **spec** for a standalone architecture (working name **elicitation - Plan, don't do: tickets resolve decisions. The spec (ticket 08) is the map's only deliverable. - Lexicon (hardened via `/domain-modeling` 2026-08-06, during the Questioning-UX grilling; canonical home: repo `CONTEXT.md`): **substrate** (the agent framework beneath: Pi family, Flue β€” the charter's "harness-agnostic core" non-goal reads "substrate-agnostic"), **ui** (the user-interface shell: whatever affords interaction β€” rendering, input, reply transport; not bound to GUI/TUI; replaces "host"/"host-interface"), **harness** (the generic capability layer: mechanism + orchestration β€” the effort's essence is harness-engineering; replaces "kernel" as shell name), **plugin** (target-defining policy; composes packs, receives harness capabilities via a narrow injected context), **pack** (unchanged: ElicitationPack = concept/observation/completion contracts; ProjectionPack = projection contract; "kernel card" survives only as the pack-content unit), **target** (split 2026-08-10 by the multi-session resolution: **target-domain** = the artifact family a plugin defines; **target-document** = the durable unit sessions attach to; bare "target" legal where context disambiguates), **issue** (typed backpressure to the elicitation controller; two producers since the capture-sweep resolution β€” plugin ops at the payload level, the harness itself at the envelope level). - **Built artifacts as proofs** (ratified 2026-08-06, from the Questioning-UX grilling): prototypes, walking skeletons, and logic-prototypes are the proof path β€” grilling tickets resolve decisions _provisionally_ and name the proof obligations they delegate to prototype tickets, rather than pinning everything through precedent + conversation alone. -- Primary references: [docs/reference/agentic-elicitation-challenges-2026-08-06T10-02-41Z.md](../../../reference/agentic-elicitation-challenges-2026-08-06T10-02-41Z.md) (turn 1: four contracts, packs, IR) and [docs/reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md](../../../reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md) (turn 2: hourglass, proof obligations, invariants, smells, test matrix) +- Primary references: [docs/reference/agentic-elicitation-challenges-2026-08-06T10-02-41Z.md](../../reference/agentic-elicitation-challenges-2026-08-06T10-02-41Z.md) (turn 1: four contracts, packs, IR) and [docs/reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md](../../reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md) (turn 2: hourglass, proof obligations, invariants, smells, test matrix) - Reference codebase (read-only): `../brunch` β€” esp. `src/agents/runtime/elicitor/`, `src/exchanges/`, `src/agents/skills/` - External: [Flue docs](https://flueframework.com/docs/guide/why-flue/), [zil-lean](https://github.com/jagg-ix/zil-lean) @@ -37,7 +37,7 @@ A reviewable **spec** for a standalone architecture (working name **elicitation -- [Assemble the spec](issues/08-assemble-the-spec.md) β€” **the destination: [spec.md](../../../spec.md) assembled** (fourteen sections + adjudications appendix) from the full working set in amendment order; all seven pre-pass contradictions adjudicated (storage port binding-implemented with the `db.ts` reconciliation; ops stay pure; status derived never stored, retraction specified; tap-ness as a transport fact via a reserved reply encoding; invariant 1 reconciled with the provenance rule; per-message current-affordance surface with identity on tool output parts; no instruction interpolation + harness-mechanical reply binding, no echo token); ten harness invariants restated in envelope vocabulary; `CONTEXT.md` gained the envelope vocabulary and the kernel-compound ruling (kernel card kept, "kernel invariants" β†’ harness invariants); rename propagated over tickets 02/07. **The frontier is empty β€” this map is complete.** Remaining fog is post-spec and graduates with the build effort. +- [Assemble the spec](issues/08-assemble-the-spec.md) β€” **the destination: [spec.md](../../specs/elicitation-kernel.md) assembled** (fourteen sections + adjudications appendix) from the full working set in amendment order; all seven pre-pass contradictions adjudicated (storage port binding-implemented with the `db.ts` reconciliation; ops stay pure; status derived never stored, retraction specified; tap-ness as a transport fact via a reserved reply encoding; invariant 1 reconciled with the provenance rule; per-message current-affordance surface with identity on tool output parts; no instruction interpolation + harness-mechanical reply binding, no echo token); ten harness invariants restated in envelope vocabulary; `CONTEXT.md` gained the envelope vocabulary and the kernel-compound ruling (kernel card kept, "kernel invariants" β†’ harness invariants); rename propagated over tickets 02/07. **The frontier is empty β€” this map is complete.** Remaining fog is post-spec and graduates with the build effort. - [Walking skeleton: sweep seam on Flue](issues/13-walking-skeleton-sweep-seam.md) β€” **all four capabilities hold on Flue** (two native, two binding-absorbed, none forbidden), closing pre-pass L1–L3: settlement trigger = `useAgentFinish` would-stop seam with same-response signal steering (fires on `terminate:true` suspensions too β€” the pending guard is load-bearing; needs a re-nudge loop guard); session-entry read = binding-absorbed via the public durable-history projection (no in-process API; `purpose` discriminates provenance) with the load-bearing HITL amendment that **evidence anchoring is harness-resolved β€” the model cites verbatim user quotes, never entry numbers** (sharpens issue 12's span shape: excerpt primary, pointer derived); on-behalf injection = `kind:'signal'` append/dispatch, projecting structurally non-user so the citable-evidence rule is mechanically enforced (live: unscripted conversational conflict-surfacing off the re-entry briefing; a fact recovered from a _superseded_ capture, honestly marked inferred); transactional store = binding-owned entirely (content-keyed idempotence is load-bearing under Flue's at-least-once re-execution). **Amends Shipping shape's capability list to ten entries** (adds would-stop seam, entry-projection read, signal injection, transactional store). New payload-stratum spec examples: absence smuggled as payload; compound payloads make supersession lossy. Prototype on branch `prototype/13-sweep-seam`. - [Multi-session elicitation & durable target state](issues/12-multi-session-durable-target.md) β€” **durable target-document, transient sessions, sweep as the only bridge**: "target" splits into **target-domain** (family a plugin defines) / **target-document** (durable unit sessions attach to β€” capture store **plus all session logs** as truth, renders strictly derived); session = one substrate conversation (per-session state: log + swept high-water mark + pending-affordance slot), never formally closes; **interleaved-only** concurrency β€” serialized store, whole-sweep-atomic refusals, the single-hop refusal doubling as the stale-session guard (true concurrency + per-item application β†’ fog); re-entry briefing = injected on-behalf-of-user state messages (Pi convention), advisory-only, with the sharpened provenance rule that **only true user entries are citable evidence**; completion = derived status, never a write gate; **persistence hypothesis flipped** β€” storage port harness-defined, binding-implemented, plugin-blind (plugin-addressable only via PluginContext-passed methods); evidence spans = pointer + quoted excerpt. - [Shipping shape: kernel library vs. Flue agent](issues/06-shipping-shape.md) β€” **harness library in a thin host-authored agent, ratified with the second-binding test**: the spec enumerates a six-item substrate-capability list as the core/binding seam (portability = pressure test, not build target; **binding** entered the glossary); topology = Bun-workspace monorepo in this repo β€” `packages/core` (plugin SDK is its export surface; `core/testing` subpath), `packages/flue` (binding), `packages/plugin-gherkin` + `packages/plugin-assurance` (renamed from `plugin-proof-obligations` 2026-08-10, per the Formal-verification canon survey's category-error verdict; plugins depend on core _only_ β€” stated spec invariant), `apps/dev`; Valibot throughout; tool prefix from product name (provisionally `bl_*`, never `elicit_*` β€” strings name identity, not function); workspace-internal, no publishing; testing = **generation-first fixtures over a deterministic replay driver** (kernel invariants as properties, `arbitraryFromSchema`, `fc.commands` conversation fuzzing from the envelope vocabulary, model as offline generator never CI oracle, regenerate-on-declaration-change); dev app chartered with three roles (dev loop / target-gallery demo / diagnostic probe views); one agent per target; milestone one local-only with remote-parity constraints pinned in spec; CI smoke = build + simulation suite. diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/notes/consistency-prepass-2026-08-10.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/notes/consistency-prepass-2026-08-10.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/notes/consistency-prepass-2026-08-10.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/notes/consistency-prepass-2026-08-10.md diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/product-description-plain.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/product-description-plain.md similarity index 98% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/product-description-plain.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/product-description-plain.md index 9aad7cd26eb..634c12f4078 100644 --- a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/product-description-plain.md +++ b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/product-description-plain.md @@ -1,6 +1,6 @@ # Elicitation Kernel β€” Product Description (plain prose edition) -Status: companion document to [spec.md](../../../spec.md); an alternate rendering of +Status: companion document to [spec.md](../../specs/elicitation-kernel.md); an alternate rendering of [product-description.md](product-description.md) in plain technical prose rather than Simplified Technical English. Written: 2026-08-10 diff --git a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/product-description.md b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/product-description.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/product-description.md rename to libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/product-description.md index 4edd1c52b3d..82a9e62592a 100644 --- a/libs/@hashintel/brunch-agent/docs/history/planning/elicitation-kernel/product-description.md +++ b/libs/@hashintel/brunch-agent/docs/archive/elicitation-kernel/product-description.md @@ -1,6 +1,6 @@ # Elicitation Kernel β€” Product Description -Status: companion document to [spec.md](../../../spec.md) +Status: companion document to [spec.md](../../specs/elicitation-kernel.md) Written: 2026-08-10 This document tells you what the product does. It uses product terms, not architecture terms. diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/refactor-queue-2026-08-14.md b/libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/refactor-queue-2026-08-14.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/refactor-queue-2026-08-14.md rename to libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/refactor-queue-2026-08-14.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/remediation-plan-2026-08-17.md b/libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/remediation-plan-2026-08-17.md similarity index 96% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/remediation-plan-2026-08-17.md rename to libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/remediation-plan-2026-08-17.md index acca05f1eec..8b4841e56fc 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/remediation-plan-2026-08-17.md +++ b/libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/remediation-plan-2026-08-17.md @@ -32,8 +32,8 @@ FE-1392 supplied the intended `useInitialData` caller for immutable session-to-t | # | Question | Why it matters | Mode | Owner | | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -| B1 | Capability 8's contract: what exactly does the durable entry projection's GET surface guarantee (paging, entry kinds, offsets) when read self-HTTP from in-process? | The binding's biggest docs-unsupported bet; FE-1391's archive and anchoring both stand on it | **Resolved and implemented (2026-08-18):** `history()` is one unpaged materialized-message snapshot; updates page at 100 durable batches and the SDK drains them, but chunks are UI-only protocol; 1,000 is an internal store cap, not a wire entry limit. The binding consumes `history()` through a host-injected full URL resolver and transport. See [source-read record](flue-entry-projection-source-read-2026-08-18.md) | **FE-1391 implemented:** archive-owned ordinals, identity-keyed versions, and actual injected-transport read are pinned in binding tests | -| B2 | Does compaction preserve the durable entry projection (history retention through compaction)? | Every long interview; evidence pointers bind to the projection | **Source-settled for Flue 2.0.3 (2026-08-18):** canonical storage is append-only; compaction appends a record and rewrites model context only; public history and persistent state survive. Reshape FE-1386 to one behavioral upgrade pin rather than an open-ended spike. See [source-read record](flue-entry-projection-source-read-2026-08-18.md) | **FE-1386:** separate narrow behavioral pin; deliberately not folded into FE-1391/FE-1392 | +| B1 | Capability 8's contract: what exactly does the durable entry projection's GET surface guarantee (paging, entry kinds, offsets) when read self-HTTP from in-process? | The binding's biggest docs-unsupported bet; FE-1391's archive and anchoring both stand on it | **Resolved and implemented (2026-08-18):** `history()` is one unpaged materialized-message snapshot; updates page at 100 durable batches and the SDK drains them, but chunks are UI-only protocol; 1,000 is an internal store cap, not a wire entry limit. The binding consumes `history()` through a host-injected full URL resolver and transport. See [source-read record](../../../evidence/proofs/audits/flue-entry-projection-source-read-2026-08-18.md) | **FE-1391 implemented:** archive-owned ordinals, identity-keyed versions, and actual injected-transport read are pinned in binding tests | +| B2 | Does compaction preserve the durable entry projection (history retention through compaction)? | Every long interview; evidence pointers bind to the projection | **Source-settled for Flue 2.0.3 (2026-08-18):** canonical storage is append-only; compaction appends a record and rewrites model context only; public history and persistent state survive. Reshape FE-1386 to one behavioral upgrade pin rather than an open-ended spike. See [source-read record](../../../evidence/proofs/audits/flue-entry-projection-source-read-2026-08-18.md) | **FE-1386:** separate narrow behavioral pin; deliberately not folded into FE-1391/FE-1392 | | B3 | Exact private model-call surface for sweep extraction β€” previously paraphrased as direct `useSubagent` invocation | FE-1392 needs private structured extraction without granting pointer authority | **Resolved and implemented (2026-08-18):** installed 2.0.3 types and official docs show `useSubagent` is declarative/model-driven through `task`; `harness.prompt(..., { result })` is the direct private structured call inside a harness tool. FE-1392 uses it inside a durable tool with `step.do` boundaries | **FE-1392 implemented** | | B4 | Can a skill-bearing agent run under `start()` in our node child process (Vite-graph resolution outside `vite build`)? | Decides whether FE-1403/06 card content is testable in our harness or needs fixture-grade copies | **Local probe**, hour-scale, when cards first compile | FE-1403/FE-1406 | | B5 | The evals guide never documents the faux-provider pattern; our CI hermeticity rests on pi-ai's contract | A pi-ai breaking change silently breaks the hermetic gate | Pin pi-ai deliberately; **upstream question** to Flue (docs suggestion: the fetch-shim + faux pattern is worth documenting) | UNOWNED (small; ride any FE-1383 slice) | diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/review-remediation-2026-08-18.md b/libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/review-remediation-2026-08-18.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/review-remediation-2026-08-18.md rename to libs/@hashintel/brunch-agent/docs/archive/engineering/legibility/review-remediation-2026-08-18.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/open-questions-elicitation-design-2026-08-11.md b/libs/@hashintel/brunch-agent/docs/archive/external-snapshots/open-questions-elicitation-design-2026-08-11.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/open-questions-elicitation-design-2026-08-11.md rename to libs/@hashintel/brunch-agent/docs/archive/external-snapshots/open-questions-elicitation-design-2026-08-11.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/expert-meeting-prep-2026-08-11.md b/libs/@hashintel/brunch-agent/docs/archive/meetings/expert-meeting-prep-2026-08-11.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/expert-meeting-prep-2026-08-11.md rename to libs/@hashintel/brunch-agent/docs/archive/meetings/expert-meeting-prep-2026-08-11.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/hash-monorepo-import-plan.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/hash-monorepo-import-plan.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/hash-monorepo-import-plan.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/hash-monorepo-import-plan.md index ef19fcd9196..63400b0fe84 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/hash-monorepo-import-plan.md +++ b/libs/@hashintel/brunch-agent/docs/archive/migrations/hash-monorepo-import-plan.md @@ -204,7 +204,7 @@ the import review checks the expanded tracked-file list against these rows befor | `docs/reference/**` | Move to `libs/@hashintel/brunch-agent/docs/reference/`; preserve provenance and existing consumers. | | `docs/INDEX.md` | Re-author as `libs/@hashintel/brunch-agent/docs/INDEX.md` over the moved living and historical set. | | External canonical entries in `docs/INDEX.md` | Preserve their URLs and `external` status in the re-authored INDEX; no external document is copied into HASH. | -| `docs/agents/**` | Re-author under `libs/@hashintel/brunch-agent/docs/agents/` for HASH paths and `gh stack`; preserve the issue-writing, triage, documentation, domain, posture, legibility, arc-close, and Flue-routing disciplines. | +| `docs/agents/**` | Re-author under `libs/@hashintel/brunch-agent/docs/agents/` for HASH paths and Graphite (`gt`); preserve the issue-writing, triage, documentation, domain, posture, legibility, arc-close, and Flue-routing disciplines. | | `AGENTS.md`, `CLAUDE.md` | Re-author as context-local guidance at `libs/@hashintel/brunch-agent/`; defer to HASH root guidance and link the re-homed agent protocols. Add pointer files under `apps/brunch-agent/`. | | `.agents/skills/arc-close/SKILL.md` | Re-author under the context root and mirror it under `.claude/skills/arc-close/`; the wrapper delegates to the canonical `docs/agents/arc-close.md`. | | `test/docs-index.test.ts` | Port to the imported package test suite and point it at the re-authored package index. | diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/README.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/README.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/README.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/README.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/apply-log.jsonl b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/apply-log.jsonl similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/apply-log.jsonl rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/apply-log.jsonl diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/github-proposals.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/github-proposals.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/github-proposals.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/github-proposals.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/github-source.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/github-source.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/github-source.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/github-source.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-canonical-target-hashes.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-canonical-target-hashes.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-canonical-target-hashes.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-canonical-target-hashes.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1366.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1366.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1366.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1366.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1383.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1383.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1383.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1383.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1401.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1401.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1401.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1401.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1406.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1406.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-proposals-FE-1406.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-proposals-FE-1406.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-source.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-source.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/linear-source.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/linear-source.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/source-record-hashes.json b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/source-record-hashes.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/data/source-record-hashes.json rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/data/source-record-hashes.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/github-editorial-review.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/github-editorial-review.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/github-editorial-review.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/github-editorial-review.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/github.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/github.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/github.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/github.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1357.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1357.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1357.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1357.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1366.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1366.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1366.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1366.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1383.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1383.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1383.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1383.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1401.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1401.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1401.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1401.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1406.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1406.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-FE-1406.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-FE-1406.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-editorial-review.md b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-editorial-review.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/review/linear-editorial-review.md rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/review/linear-editorial-review.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/validate.ts b/libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/validate.ts similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/issue-pr-migration-2026-08-20/validate.ts rename to libs/@hashintel/brunch-agent/docs/archive/migrations/issue-pr-legibility-2026-08-20/validate.ts diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/grilling-inputs-2026-08-12.md b/libs/@hashintel/brunch-agent/docs/archive/planning-inputs/grilling-inputs-2026-08-12.md similarity index 98% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/grilling-inputs-2026-08-12.md rename to libs/@hashintel/brunch-agent/docs/archive/planning-inputs/grilling-inputs-2026-08-12.md index eaa2bb1fa40..fa9ed5ce0b4 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/grilling-inputs-2026-08-12.md +++ b/libs/@hashintel/brunch-agent/docs/archive/planning-inputs/grilling-inputs-2026-08-12.md @@ -2,7 +2,7 @@ State capture ahead of the HITL tickets (FE-1361/62/63/64). Everything here is _input to decisions_, not decisions. Sources: the three research resolutions (gists on FE-1358/59/60, -full findings in [`../research/`](../research/)), the talkthrough discussion, and +full findings in [`../../reference/research/`](../../reference/research/)), the talkthrough discussion, and [Dora's PRO-98 checklist](https://www.notion.so/hashintel/3b93c81fe0248019a7beda9bb31df2c8). ## 1. Destination trend (Lu, 2026-08-12) diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/penciled-directions-2026-08-14.md b/libs/@hashintel/brunch-agent/docs/archive/planning-inputs/penciled-directions-2026-08-14.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/penciled-directions-2026-08-14.md rename to libs/@hashintel/brunch-agent/docs/archive/planning-inputs/penciled-directions-2026-08-14.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/SPEC-LEDGER.md b/libs/@hashintel/brunch-agent/docs/control/SPEC-LEDGER.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/SPEC-LEDGER.md rename to libs/@hashintel/brunch-agent/docs/control/SPEC-LEDGER.md index 226a72fac31..fc814343344 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/SPEC-LEDGER.md +++ b/libs/@hashintel/brunch-agent/docs/control/SPEC-LEDGER.md @@ -2,7 +2,7 @@ This living ledger tracks the elicitation-kernel specification's obligations against the built system. Code and tests are authoritative; a branch that changes an obligation updates its row in -the same change. Unlike `COORDINATION.md`, this document has a terminal lifecycle: it settles as +the same change. Unlike current `STEERING.md`, this document has a terminal lifecycle: it settles as a record when milestone one closes. Status vocabulary: **discharged** (built and proved) Β· **partial** (built with named gaps) Β· @@ -108,7 +108,7 @@ states. | Pack form, Principle v2 | Β§11.2 | **pending** | β€” | | Smallest honest plugin as a standing bar | Β§11.3 | **partial** | `statement-noted.test.ts` and the core plugin fixture encode the one-type verbatim floor and reject undeclared parsed/pointer shape; the standing bar must grow with FE-1393's operations | | Generic strategy quiver | Β§11.5 | **pending** (ownership repaired) | was **orphaned** β€” named-not-designed, carried by no map β€” now FE-1406 (root issue) | -| Portfolio + hybrid order: both packs authored before the pack interface freezes | Β§13 | **pending** | Owned by FE-1387 (FE-1383 slice, backlog), explicitly gated on FE-1357's second-target decision β€” the inter-map seam in miniature (see `COORDINATION.md`). Gherkin wiring ahead stays legal while FE-1387 holds the freeze | +| Portfolio + hybrid order: both packs authored before the pack interface freezes | Β§13 | **pending** | Owned by FE-1387 (FE-1383 slice, backlog); current sequencing puts the CPS proof before generic freeze (see `STEERING.md`). Gherkin wiring ahead stays legal while FE-1387 holds the freeze | | Gherkin validation (parse validity, step lexicon) | Β§13.1 | **pending** | β€” | | Assurance target (Statement record, four edges, five-stratum derivation, ledger) | Β§13.2–13.3 | **pending** | β€” | diff --git a/libs/@hashintel/brunch-agent/docs/control/STEERING.md b/libs/@hashintel/brunch-agent/docs/control/STEERING.md new file mode 100644 index 00000000000..41d6f9c3cff --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/control/STEERING.md @@ -0,0 +1,93 @@ +# Brunch steering + +This is Brunch's one mutable current strategic control. Linear owns issue facts; +[specifications](../specs/), [ADRs](../adr/), and [the ledger](SPEC-LEDGER.md) own their truths. + +## Objective and acceptance proof + +Prove a bounded CPS **review-and-revise** loop through the production Brunch and Petrinaut path: +a reviewer opens an existing source-grounded model and net, traces one element to the source +utterance, corrects it in three to five turns, and sees a provenance-preserving net delta handed to +the optimisation flow. This is the current proof, not a permanent product-scope decision. + +Acceptance is one screen-recordable deployed run, surviving reload, through the real HTTP handler, +session binding, sweep, fold, pure projection, client-tool application, and optimisation handoff. +The changed element traces to a sweep-produced superseding capture while an unrelated region stays +stable. Preserve runnable and legibility evidence under [proof evidence](../evidence/proofs/). + +Governing strategic decisions: [S-001](STRATEGY-LOG.md#s-001), +[S-002](STRATEGY-LOG.md#s-002), and [S-003](STRATEGY-LOG.md#s-003). + +## Execution tree + +```text +now +β”œβ”€ semantic: FE-1482 CPS slice + FE-1480 authority decision + FE-1478 provenance +β”‚ gap: worked capture -> model -> SDCPN transformation and production model/issues read path +└─ reviewer: FE-1438 client-tool return + FE-1439 existing-target session + gap: narrow review-and-revise controller/runbook on the real Petrinaut route +join +└─ FE-1479 targeted correction: reviewer utterance -> sweep -> supersession -> stable scoped delta +next +└─ FE-1477/FE-1440 provider routing -> FE-1423 exposure gates -> FE-1441 deployment + gap: clean-browser rehearsal and optimisation handoff +``` + +### Active soft edges + +- FE-1480 inputs FE-1482/FE-1478; semantic and reviewer lanes join only at FE-1479. +- FE-1477 and FE-1440 share one routing implementation. +- FE-1402/FE-1403/FE-1406/FE-1431 input FE-1482 only when its CPS slice consumes them. +- FE-1395's structured-tap transport fact inputs the capture-store evidence rule. +- FE-1385/FE-1404/FE-1423 share telemetry vocabulary before FE-1423's exposure gate. +- The living-prototype charter waits on the infrastructure conversation. +- The unresolved absence locator is a fold-table coordinate owned here; its cases and obligation + remain in [the plugin contract](../specs/plugin-contract.md#epistemic-anatomy). + +The read-only Linear graph supplies mechanical availability, never priority. + +## Active gates + +| Gate | Owner / source | Watch trigger | Last checked | Consequence | +| --- | --- | --- | --- | --- | +| FE-1480 register authority unresolved | FE-1480; [ADR-0003](../adr/0003-three-register-ir.md) | Worked transformation assigns every judgment. | 2026-08-24 | Do not freeze projector/contract; record semantics earlier or revisit ADR. | +| Final use case outstanding | Dora; FE-1476 / September Plan | Dora confirms or changes it. | 2026-08-24 | If creation is required, activate cold-start and reconcile ADR-0004/proof. | +| Truck-fleet dossier unavailable | Unknown; FE-1357 names its ref/path. | Ref appears or replacement selected. | 2026-08-24 | Claim no provenance; use reviewed replacement. | + +## Decision-relevant beliefs and unknowns + +| Belief or unknown | Confidence / evidence | Cheapest probe | +| --- | --- | --- | +| CPS establishes the minimum plugin contract. | Medium-high; Gherkin under-stresses it. | Implement one FE-1480 transformation. | +| Register 2 supports pure projection. | Low-medium; ADR-0003 says so, FE-1480 disputes it. | Assign each judgment; expose residue. | +| Five turns yield a scoped correction. | Low; unrehearsed. | Run two bounded rehearsals. | +| Ask carries durable client-tool results. | Medium-low; machine results refused today. | Run one correlated FE-1438 round trip. | +| Structured export explains provenance/delta. | Medium; FE-1481 permits it. | Witness one rehearsal. | + +## Sequencing cuts + +- Cold-start does not gate review-and-revise ([S-001](STRATEGY-LOG.md#s-001)). +- CPS precedes FE-1387's generic freeze; Gherkin completion does not gate it + ([S-002](STRATEGY-LOG.md#s-002)). +- Defer broad UI/ontology/gallery/affordances/voice/scenarios/telemetry until the loop closes. +- Fixtures supply domain state, never product wiring; provenance and the real entrypoint are gates. + +## Stop or replan + +- Dora requires cold-start creation. +- FE-1480 exposes unavoidable read-time semantic inference. +- Two rehearsals fail the five-turn correction. +- FE-1438 loses correlation, durability, or evidence semantics. +- Production remains undeployable after FE-1479; seek a demo-surface decision, not test wiring. + +## Exceptional roots + +- **FE-1331** β€” create-new-net entry; keep exceptional until the watched use-case decision settles + whether it joins the cold-start lane or receives a parent. +- **FE-1334** β€” surprising-scenario validation gesture; parent or cancel when its owning outcome is + chosen. +- **FE-1406** β€” intentional cross-map root for the harness strategy quiver. +- **FE-1472** β€” unrelated SDK-pin triage; assign an owning map or remove from the project. +- **FE-1476** β€” September delivery root; intended parent is FE-1357. +- **FE-1477–FE-1482** β€” PM-authored outcome roots; intended parent is FE-1476 after overlap review + and separately approved Linear mutation. diff --git a/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md b/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md new file mode 100644 index 00000000000..606a31e9d20 --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md @@ -0,0 +1,71 @@ +# Brunch strategy log + +This append-only log records material strategic choices: objective, proof frontier, authority +boundary, material cut, gate consequence, or confidence changes. It is neither an ADR (which owns +accepted architecture) nor a diary. IDs increase monotonically. Conflicting decisions append an +entry naming the superseded governing ID; complementary decisions use `none`. Existing entries are +immutable except for typo/link repair, and only unsuperseded governing IDs appear in `STEERING`. + +## Entries + +### S-001 + +**Date:** 2026-08-24 + +**Trigger/evidence:** FE-1476 defines a bounded reviewer correction scenario; the final use-case +decision remains outstanding. + +**Decision:** Review-and-revise is the September proof. Full cold-start remains an important +quality benchmark and contingency lane, not a gate on this proof. + +**Consequences/cuts:** Build against an existing source-grounded target now; activate cold-start if +the use-case decision changes. This does not define permanent product scope. + +**Revisit when:** Dora confirms or changes the use case, or the prepared target cannot support the +optimisation handoff. + +**Supersedes:** none + +**Evidence links:** [STEERING objective](STEERING.md#objective-and-acceptance-proof), FE-1476, +[ADR-0004](../adr/0004-in-petrinaut-staging-and-the-monorepo-import.md) + +### S-002 + +**Date:** 2026-08-24 + +**Trigger/evidence:** The Gherkin floor deliberately under-stresses the provisional plugin contract; +FE-1480 remains unresolved and CPS is its first material consumer. + +**Decision:** The worked CPS slice establishes the minimum exercised plugin contract before a +generic contract freeze. + +**Consequences/cuts:** FE-1482 pressures the interface first; FE-1387 and broad generic/Gherkin +completion follow the proof rather than gate it. + +**Revisit when:** The worked CPS transformation requires a reusable harness primitive that must +precede the slice. + +**Supersedes:** none + +**Evidence links:** [plugin contract](../specs/plugin-contract.md), +[ADR-0003](../adr/0003-three-register-ir.md), FE-1480, FE-1482 + +### S-003 + +**Date:** 2026-08-24 + +**Trigger/evidence:** CPS semantics and reviewer transport/session work have independent early +risks, but neither proves value until a real correction crosses both. + +**Decision:** Run semantic and reviewer lanes in parallel and join them at targeted correction +before provider routing and deployment. + +**Consequences/cuts:** FE-1482/FE-1480/FE-1478 and FE-1438/FE-1439 may proceed concurrently; +FE-1479 is the mandatory join, followed by FE-1477/FE-1440 and FE-1441. + +**Revisit when:** Either lane fails its first production-path probe or FE-1479 exposes a missing +shared prerequisite. + +**Supersedes:** none + +**Evidence links:** [STEERING execution tree](STEERING.md#execution-tree), FE-1479 diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/readout.md b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/readout.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/readout.md rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/readout.md index 36b7f5b8e89..1dd756efcdf 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/readout.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/readout.md @@ -1,7 +1,9 @@ # Baseline control β€” read-out (FE-1361) Scored 2026-08-13, against the transcripts in [`transcripts/`](transcripts/). Design and -mechanics in [protocol.md](protocol.md). Both conditions ran `claude-opus-5` as interviewer +mechanics in the executable +[protocol](../../../../../evaluations/protocols/process-model-elicitation/baseline/protocol.md). +Both conditions ran `claude-opus-5` as interviewer against the same simulated master scheduler, single-shot each β€” every claim below is existence evidence from one run per condition, not a rate estimate. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-1-model.txt b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-1-model.txt similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-1-model.txt rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-1-model.txt diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-1.md b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-1.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-1.md rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-1.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-1.raw.json b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-1.raw.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-1.raw.json rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-1.raw.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-2-model.txt b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-2-model.txt similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-2-model.txt rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-2-model.txt diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-2.md b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-2.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-2.md rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-2.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-2.raw.json b/libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-2.raw.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/transcripts/condition-2.raw.json rename to libs/@hashintel/brunch-agent/docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/condition-2.raw.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/deep-read-fe-1389.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/deep-read-fe-1389.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/deep-read-fe-1389.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/deep-read-fe-1389.md index 92c765b3b34..e962d5002f7 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/deep-read-fe-1389.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/deep-read-fe-1389.md @@ -81,7 +81,7 @@ The five asserted properties: the bound reply text and the `affordance-reply-bou 4. **The kickoff message is a machine-authored _user_ entry.** Both `chat.tsx:67` and the test send `{ kind: 'user', body: 'Begin the interview.' }`. Per Β§9.4 only true user entries are citable as capture evidence β€” and this one is structurally a true user entry that no user typed. Harmless until sweeps land; then it is an anchorable non-utterance. The correct carrier is the signal channel this branch just built. 5. **No user-visible insertion notice.** The `affordance-reply-bound` signal projects `display: 'diagnostic'` (ticket 13 Β§3) and `chat.tsx:11-16` filters it out. Β§9.3 requires "a minimal user-visible insertion notice accompanies every injected state message"; this is the first injected entry in the system and it ships silent. Scoped to briefings in the spec's letter, so not yet a violation, but the same code path will owe it. -**Brittle oracles** β€” and the repo already knows: `docs/planning/legibility-sweep/refactor-queue-2026-08-14.md` item 2 names the faux-provider marker comment specifically. +**Brittle oracles** β€” and the repo already knows: `docs/archive/engineering/legibility/refactor-queue-2026-08-14.md` item 2 names the faux-provider marker comment specifically. 1. **`noInstructionWake` is a case-insensitive substring absence over `JSON.stringify(history.messages)`.** It matches Flue's advisory _phrasing_. If Flue rewords the advisory, the check passes forever while the wart is back β€” and this check is the one that deletes the `wake-wart-residue` entry from `test/known-gaps.ts`. 2. **`boundReplyReachedModel` under-asserts the binding.** It is two substring tests over the stringified context. It does not check that the signal carries the _right_ `affordanceId`, that it quotes the _first_ question rather than the second, or that it sits adjacent to the user message. The property "the correct affordance was bound" is not actually pinned. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/deep-read-fe-1390.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/deep-read-fe-1390.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/deep-read-fe-1390.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/deep-read-fe-1390.md index 28784343357..531b1629413 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/deep-read-fe-1390.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/deep-read-fe-1390.md @@ -4,7 +4,8 @@ Deep-read of commit 02fd352 (branch `ln/fe-1390-capture-store`, PR #11), produce remediation sweep (FE-1401): builder's account, spec-discharge note, write-time tiering assessment against penciled item 7, the commit-message backfill (applied to the branch), and a live-probed verification of the FE-1419 refactor queue's capture-store claims. Agent-authored -under instruction; reviewed before landing. Companion rendering: `../capture-store-plain.md`. +under instruction; reviewed before landing. Companion rendering: +[`capture-store.md`](../../../reference/architecture/capture-store.md). ## Builder's account diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/flue-entry-projection-source-read-2026-08-18.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/flue-entry-projection-source-read-2026-08-18.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/flue-entry-projection-source-read-2026-08-18.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/flue-entry-projection-source-read-2026-08-18.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/flue-patterns-audit-2026-08-17.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/flue-patterns-audit-2026-08-17.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/legibility-sweep/flue-patterns-audit-2026-08-17.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/flue-patterns-audit-2026-08-17.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/research-patterns-audit.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/research-patterns-audit.md similarity index 98% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/research-patterns-audit.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/research-patterns-audit.md index a9b18bad17a..0e876496316 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/research-patterns-audit.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/proofs/audits/research-patterns-audit.md @@ -1,6 +1,6 @@ # The research patterns, in plain language -> A plain-prose audit of the patterns, principles, and instruments this effort has imported from the research literature β€” the vocabulary that appears in the v0 prompt, the baseline readout, the IR design, and the map as compressed tokens. Each entry says what the thing is, where it comes from and how strong the evidence is, and how this effort uses it. The underlying research documents ([`elicitation-strategy-literature.md`](../research/elicitation-strategy-literature.md) and the [worker report](../research/re-interviewing-literature-worker-report.md)) remain authoritative; this is a review-reading rendering. The audit pass surfaced eight strain points β€” places where an import is softer, drifted, or more house-made than our gists state β€” recorded in the appendix at the end of this document and on FE-1401 (seventh accrual). +> A plain-prose audit of the patterns, principles, and instruments this effort has imported from the research literature β€” the vocabulary that appears in the v0 prompt, the baseline readout, the IR design, and the map as compressed tokens. Each entry says what the thing is, where it comes from and how strong the evidence is, and how this effort uses it. The underlying research documents ([`elicitation-strategy-literature.md`](../../../reference/research/elicitation/elicitation-strategy-literature.md) and the [worker report](../../../reference/research/elicitation/interviewing-literature-source-catalog.md)) remain authoritative; this is a review-reading rendering. The audit pass surfaced eight strain points β€” places where an import is softer, drifted, or more house-made than our gists state β€” recorded in the appendix at the end of this document and on FE-1401 (seventh accrual). The effort mined five research fields for guidance on how an AI interviewer should elicit a process model: knowledge acquisition (the old expert-systems tradition of getting knowledge out of specialists), requirements engineering interviewing, simulation conceptual modelling, structured expert judgment (the decision-analysis discipline of getting honest numbers from experts), and the brand-new literature on LLM-conducted interviews. What follows covers roughly thirty imports, grouped by the job they do. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-worked-examples.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/design/intermediate-representation-worked-examples.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-worked-examples.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/design/intermediate-representation-worked-examples.md index 76a09b3966e..f56bc55e966 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-worked-examples.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/proofs/design/intermediate-representation-worked-examples.md @@ -1,7 +1,7 @@ # IR worked examples β€” Layer-A validation (FE-1397) Resolved 2026-08-13. This document discharges the ratification condition on the generic IR -definition ([`ir-design.md`](./ir-design.md), Layer A): speculative payload type systems drafted +definition ([`ir-design.md`](../../../specs/intermediate-representation.md), Layer A): speculative payload type systems drafted across three plugin targets at different complexity levels, each checked against the five MUST properties and three MAY patterns. **Desk validation only** β€” nothing here has run through a working harness; Layer-A claims stay provisional until the September build exercises them. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/transport-aisdk-implementation-2026-08-19.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/implementations/fe-1436-transport-aisdk-2026-08-19.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/transport-aisdk-implementation-2026-08-19.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/implementations/fe-1436-transport-aisdk-2026-08-19.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ask-return-implementation-2026-08-19.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/implementations/fe-1449-ask-return-2026-08-19.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ask-return-implementation-2026-08-19.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/implementations/fe-1449-ask-return-2026-08-19.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/expert-meeting-findings-2026-08-11.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/research/expert-meeting-findings-2026-08-11.md similarity index 97% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/expert-meeting-findings-2026-08-11.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/research/expert-meeting-findings-2026-08-11.md index 9ce39739fb1..bfcfed3fb2e 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/notes/expert-meeting-findings-2026-08-11.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/proofs/research/expert-meeting-findings-2026-08-11.md @@ -1,7 +1,7 @@ # Findings β€” Yannis/Dora/Lu meeting, 2026-08-11 Source: [transcript](../../../reference/yannis-dora-lu-transcript-2026-08-11.md). Prep doc: -[expert-meeting-prep-2026-08-11](expert-meeting-prep-2026-08-11.md). Participants: Lu, Yannis +[expert-meeting-prep-2026-08-11](../../../archive/meetings/expert-meeting-prep-2026-08-11.md). Participants: Lu, Yannis Zachos (Petrinaut team), Dora Ma (PM, "Speaker 4"). ## Facts established (map-shaping) diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/spikes/fe-1434-suspension-evidence-2026-08-19.json b/libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1434-suspension-evidence-2026-08-19.json similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/spikes/fe-1434-suspension-evidence-2026-08-19.json rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1434-suspension-evidence-2026-08-19.json diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/spikes/fe-1434-suspension-verdict-2026-08-19.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1434-suspension-verdict-2026-08-19.md similarity index 97% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/spikes/fe-1434-suspension-verdict-2026-08-19.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1434-suspension-verdict-2026-08-19.md index fbd6cf5eb46..b9a1ae1b339 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/spikes/fe-1434-suspension-verdict-2026-08-19.md +++ b/libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1434-suspension-verdict-2026-08-19.md @@ -28,7 +28,7 @@ for the protocol ticket rather than a behavioral claim from this spike. Two cases ran: three returned tool outputs, retained as a normalized before/after transcript, and 100 returned tool outputs, pressure-testing the count raised by the -[Petrinaut survey Β§6d](../research/petrinaut-survey.md#6d-where-the-evidence-is-thin) for a +[Petrinaut survey Β§6d](../../../reference/research/petrinaut-survey.md#6d-where-the-evidence-is-thin) for a roughly 40-place net. These are faux outputs, not 100 real dependent Petrinaut mutations. The probe then read public Flue history through the binding's [`projectFlueHistoryForSweep`](../../../../packages/binding-flue/src/history-reader.ts) function @@ -64,7 +64,7 @@ parallel design lacks value. As a recommended guardrail, initially reject a mode to suspend both a user question and a client-tool batch. Mixed-pending semantics remain untested. FE-1435 (the panel-to-AI-SDK adapter spike named by the -[integration spec](../petrinaut-integration-spec.md#testing-decisions)) still has to prove that +[integration spec](../../../specs/petrinaut-integration.md#testing-decisions)) still has to prove that the real panel's batched tool-result POST can become this one Flue signal without losing ids. That is the exact remaining wire uncertainty; it does not reopen the Flue runtime behavior tested here. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/adapter-panel-spike-2026-08-19.md b/libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1435-adapter-panel-2026-08-19.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/adapter-panel-spike-2026-08-19.md rename to libs/@hashintel/brunch-agent/docs/evidence/proofs/spikes/fe-1435-adapter-panel-2026-08-19.md diff --git a/libs/@hashintel/brunch-agent/docs/inbox/.gitkeep b/libs/@hashintel/brunch-agent/docs/inbox/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-consumption-trigger-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-consumption-trigger-layout.json new file mode 100644 index 00000000000..fd8d6e3fadd --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-consumption-trigger-layout.json @@ -0,0 +1,486 @@ +{ + "places": [ + { + "id": "place__idle_tankers", + "name": "IdleTankers", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1470, + "y": 555 + }, + { + "id": "place__loads_delivered", + "name": "LoadsDelivered", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1230, + "y": 435 + }, + { + "id": "place__s1_order_placed", + "name": "SteadyNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1785, + "y": 780 + }, + { + "id": "place__s1_order_permits", + "name": "SteadyNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1230, + "y": 660 + }, + { + "id": "place__s1_on_route", + "name": "SteadyNitrogenOnRoute", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2325, + "y": 780 + }, + { + "id": "place__s1_vented", + "name": "SteadyNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1230, + "y": 225 + }, + { + "id": "place__s1_line_running", + "name": "SteadyNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 75, + "y": 1065 + }, + { + "id": "place__s1_line_stopped", + "name": "SteadyNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 645, + "y": 1260 + }, + { + "id": "place__s1_stockouts", + "name": "SteadyNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 645, + "y": 960 + }, + { + "id": "place__s1_consumed", + "name": "SteadyNitrogenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 645, + "y": 765 + }, + { + "id": "place__s1_evaporated", + "name": "SteadyNitrogenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 630, + "y": 75 + }, + { + "id": "place__s1_contents", + "name": "SteadyNitrogenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 60, + "y": 360 + }, + { + "id": "place__s1_ullage", + "name": "SteadyNitrogenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 630, + "y": 375 + }, + { + "id": "place__s1_units_drawn_since_order", + "name": "SteadyNitrogenUnitsDrawnSinceOrder", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1050, + "y": 780 + } + ], + "transitions": [ + { + "id": "transition__s1_draw", + "name": "Draw a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_consumed", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + }, + { + "placeId": "place__s1_units_drawn_since_order", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// Switched off in the scenario where the customer's plant is shut. The tank\n// still boils off while they draw nothing, which is the point of the level.\nexport default Lambda((input, parameters) => {\n return parameters.draw_enabled > 0;\n});", + "transitionKernelCode": "", + "x": 360, + "y": 555 + }, + { + "id": "transition__s1_boil_off", + "name": "Boil off a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_evaporated", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 345, + "y": 165 + }, + { + "id": "transition__s1_raise_order", + "name": "Raise an order, 8 units drawn (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_units_drawn_since_order", + "weight": 8, + "type": "standard" + }, + { + "placeId": "place__s1_order_permits", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 1530, + "y": 780 + }, + { + "id": "transition__s1_dispatch", + "name": "Dispatch a tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 2055, + "y": 780 + }, + { + "id": "transition__s1_arrive", + "name": "Unload the tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 12 + }, + { + "placeId": "place__s1_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__idle_tankers", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 900, + "y": 555 + }, + { + "id": "transition__s1_vent", + "name": "Vent through the relief valve (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_vented", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 900, + "y": 225 + }, + { + "id": "transition__s1_stop_line", + "name": "Stop the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s1_stockouts", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 360, + "y": 1065 + }, + { + "id": "transition__s1_resume_line", + "name": "Resume the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 930, + "y": 1260 + } + ], + "types": [], + "differentialEquations": [], + "parameters": [ + { + "id": "param__draw_enabled", + "name": "Draw enabled", + "variableName": "draw_enabled", + "type": "real", + "defaultValue": "1" + } + ], + "scenarios": [ + { + "id": "scenario__drawing", + "name": "Customer drawing normally", + "description": "The customer is using product, so consumption events happen and either ordering policy has something to work with.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 1 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SteadyNitrogenUnitsDrawnSinceOrder: 0,\n};" + } + }, + { + "id": "scenario__shut", + "name": "Customer shut, tank still evaporating", + "description": "The customer's plant is down for maintenance and draws nothing. The tank still loses product to boil-off. A real operating condition, and where the two ordering policies come apart.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 0 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SteadyNitrogenUnitsDrawnSinceOrder: 0,\n};" + } + } + ], + "metrics": [ + { + "id": "metric__deliveries", + "name": "Loads delivered", + "description": "Tanker drops made.", + "code": "return state.places.LoadsDelivered.count;" + }, + { + "id": "metric__stockouts", + "name": "Stockouts", + "description": "Times a customer line stopped for want of product.", + "code": "return state.places.SteadyNitrogenStockouts.count;" + }, + { + "id": "metric__vented", + "name": "Vented through relief", + "description": "Units lost through a relief valve, in units at every level. Below the continuous levels the Vented place holds one token per unit, because the tank is a pile of one-unit tokens. From the continuous levels it holds one token per valve opening, each worth vent_loss units, so the count is scaled to keep this metric comparable down the sequence. Structurally reachable everywhere, and unreachable in practice below the continuous levels: under a level trigger the tank never fills completely, and with no pressure nothing else opens the valve.", + "code": "return state.places.SteadyNitrogenVented.count;" + }, + { + "id": "metric__evaporated", + "name": "Evaporated", + "description": "Units lost to boil-off.", + "code": "return state.places.SteadyNitrogenEvaporated.count;" + }, + { + "id": "metric__consumed", + "name": "Consumed", + "description": "Units the customers actually used.", + "code": "return state.places.SteadyNitrogenConsumed.count;" + }, + { + "id": "metric__envelope", + "name": "Contents plus ullage", + "description": "The place invariant, summed over the three sites. Every transition that moves product moves it between Contents and Ullage, so this is total capacity in every reachable marking and can be checked without simulating anything.", + "code": "return state.places.SteadyNitrogenContents.count + state.places.SteadyNitrogenUllage.count;" + }, + { + "id": "metric__stranded", + "name": "Stranded customers", + "description": "Customers that ended stopped with no order outstanding: the line is down and nothing is on its way to fix it.", + "code": "return (state.places.SteadyNitrogenLineStopped.count > 0 && state.places.SteadyNitrogenOrderPlaced.count === 0 ? 1 : 0);" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Gases 1 β€” plain net, one customer (consumption trigger)" +} diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-layout.json new file mode 100644 index 00000000000..d4b42b96572 --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-layout.json @@ -0,0 +1,472 @@ +{ + "places": [ + { + "id": "place__idle_tankers", + "name": "IdleTankers", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1455, + "y": 450 + }, + { + "id": "place__loads_delivered", + "name": "LoadsDelivered", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1230, + "y": 360 + }, + { + "id": "place__s1_order_placed", + "name": "SteadyNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1755, + "y": 675 + }, + { + "id": "place__s1_order_permits", + "name": "SteadyNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1245, + "y": 570 + }, + { + "id": "place__s1_on_route", + "name": "SteadyNitrogenOnRoute", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2280, + "y": 675 + }, + { + "id": "place__s1_vented", + "name": "SteadyNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1230, + "y": 195 + }, + { + "id": "place__s1_line_running", + "name": "SteadyNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 105, + "y": 900 + }, + { + "id": "place__s1_line_stopped", + "name": "SteadyNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 675, + "y": 1155 + }, + { + "id": "place__s1_stockouts", + "name": "SteadyNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 675, + "y": 810 + }, + { + "id": "place__s1_consumed", + "name": "SteadyNitrogenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 675, + "y": 585 + }, + { + "id": "place__s1_evaporated", + "name": "SteadyNitrogenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 675, + "y": 45 + }, + { + "id": "place__s1_contents", + "name": "SteadyNitrogenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 105, + "y": 330 + }, + { + "id": "place__s1_ullage", + "name": "SteadyNitrogenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 675, + "y": 315 + } + ], + "transitions": [ + { + "id": "transition__s1_draw", + "name": "Draw a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_consumed", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// Switched off in the scenario where the customer's plant is shut. The tank\n// still boils off while they draw nothing, which is the point of the level.\nexport default Lambda((input, parameters) => {\n return parameters.draw_enabled > 0;\n});", + "transitionKernelCode": "", + "x": 405, + "y": 510 + }, + { + "id": "transition__s1_boil_off", + "name": "Boil off a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_evaporated", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 390, + "y": 135 + }, + { + "id": "transition__s1_raise_order", + "name": "Raise an order, level below trigger (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_permits", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 16, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 1500, + "y": 675 + }, + { + "id": "transition__s1_dispatch", + "name": "Dispatch a tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 2025, + "y": 675 + }, + { + "id": "transition__s1_arrive", + "name": "Unload the tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 12 + }, + { + "placeId": "place__s1_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__idle_tankers", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 945, + "y": 450 + }, + { + "id": "transition__s1_vent", + "name": "Vent through the relief valve (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_vented", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 945, + "y": 195 + }, + { + "id": "transition__s1_stop_line", + "name": "Stop the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s1_stockouts", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 405, + "y": 900 + }, + { + "id": "transition__s1_resume_line", + "name": "Resume the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => {\n return true;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 1155 + } + ], + "types": [], + "differentialEquations": [], + "parameters": [ + { + "id": "param__draw_enabled", + "name": "Draw enabled", + "variableName": "draw_enabled", + "type": "real", + "defaultValue": "1" + } + ], + "scenarios": [ + { + "id": "scenario__drawing", + "name": "Customer drawing normally", + "description": "The customer is using product, so consumption events happen and either ordering policy has something to work with.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 1 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n};" + } + }, + { + "id": "scenario__shut", + "name": "Customer shut, tank still evaporating", + "description": "The customer's plant is down for maintenance and draws nothing. The tank still loses product to boil-off. A real operating condition, and where the two ordering policies come apart.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 0 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n};" + } + } + ], + "metrics": [ + { + "id": "metric__deliveries", + "name": "Loads delivered", + "description": "Tanker drops made.", + "code": "return state.places.LoadsDelivered.count;" + }, + { + "id": "metric__stockouts", + "name": "Stockouts", + "description": "Times a customer line stopped for want of product.", + "code": "return state.places.SteadyNitrogenStockouts.count;" + }, + { + "id": "metric__vented", + "name": "Vented through relief", + "description": "Units lost through a relief valve, in units at every level. Below the continuous levels the Vented place holds one token per unit, because the tank is a pile of one-unit tokens. From the continuous levels it holds one token per valve opening, each worth vent_loss units, so the count is scaled to keep this metric comparable down the sequence. Structurally reachable everywhere, and unreachable in practice below the continuous levels: under a level trigger the tank never fills completely, and with no pressure nothing else opens the valve.", + "code": "return state.places.SteadyNitrogenVented.count;" + }, + { + "id": "metric__evaporated", + "name": "Evaporated", + "description": "Units lost to boil-off.", + "code": "return state.places.SteadyNitrogenEvaporated.count;" + }, + { + "id": "metric__consumed", + "name": "Consumed", + "description": "Units the customers actually used.", + "code": "return state.places.SteadyNitrogenConsumed.count;" + }, + { + "id": "metric__envelope", + "name": "Contents plus ullage", + "description": "The place invariant, summed over the three sites. Every transition that moves product moves it between Contents and Ullage, so this is total capacity in every reachable marking and can be checked without simulating anything.", + "code": "return state.places.SteadyNitrogenContents.count + state.places.SteadyNitrogenUllage.count;" + }, + { + "id": "metric__stranded", + "name": "Stranded customers", + "description": "Customers that ended stopped with no order outstanding: the line is down and nothing is on its way to fix it.", + "code": "return (state.places.SteadyNitrogenLineStopped.count > 0 && state.places.SteadyNitrogenOrderPlaced.count === 0 ? 1 : 0);" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Gases 1 β€” plain net, one customer" +} diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-2-spn-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-2-spn-layout.json new file mode 100644 index 00000000000..31eb77a1c1a --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-2-spn-layout.json @@ -0,0 +1,978 @@ +{ + "places": [ + { + "id": "place__idle_tankers", + "name": "IdleTankers", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2325, + "y": 2145 + }, + { + "id": "place__loads_delivered", + "name": "LoadsDelivered", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1905, + "y": 1935 + }, + { + "id": "place__returning", + "name": "Returning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1905, + "y": 2145 + }, + { + "id": "place__s1_order_placed", + "name": "SteadyNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2325, + "y": 1575 + }, + { + "id": "place__s1_order_permits", + "name": "SteadyNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1845, + "y": 1455 + }, + { + "id": "place__s1_on_route", + "name": "SteadyNitrogenOnRoute", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2835, + "y": 1575 + }, + { + "id": "place__s1_vented", + "name": "SteadyNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1815, + "y": 1020 + }, + { + "id": "place__s1_line_running", + "name": "SteadyNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 675, + "y": 1845 + }, + { + "id": "place__s1_line_stopped", + "name": "SteadyNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1275, + "y": 2055 + }, + { + "id": "place__s1_stockouts", + "name": "SteadyNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1260, + "y": 1740 + }, + { + "id": "place__s1_consumed", + "name": "SteadyNitrogenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1260, + "y": 1560 + }, + { + "id": "place__s1_evaporated", + "name": "SteadyNitrogenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1275, + "y": 900 + }, + { + "id": "place__s1_contents", + "name": "SteadyNitrogenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 675, + "y": 1245 + }, + { + "id": "place__s1_ullage", + "name": "SteadyNitrogenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1275, + "y": 1140 + }, + { + "id": "place__s2_order_placed", + "name": "SlowNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2355, + "y": 3015 + }, + { + "id": "place__s2_order_permits", + "name": "SlowNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1860, + "y": 2910 + }, + { + "id": "place__s2_on_route", + "name": "SlowNitrogenOnRoute", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2895, + "y": 3015 + }, + { + "id": "place__s2_vented", + "name": "SlowNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1860, + "y": 2385 + }, + { + "id": "place__s2_line_running", + "name": "SlowNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 705, + "y": 3150 + }, + { + "id": "place__s2_line_stopped", + "name": "SlowNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1305, + "y": 3300 + }, + { + "id": "place__s2_stockouts", + "name": "SlowNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1305, + "y": 3030 + }, + { + "id": "place__s2_consumed", + "name": "SlowNitrogenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1305, + "y": 2865 + }, + { + "id": "place__s2_evaporated", + "name": "SlowNitrogenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1275, + "y": 2235 + }, + { + "id": "place__s2_contents", + "name": "SlowNitrogenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 705, + "y": 2565 + }, + { + "id": "place__s2_ullage", + "name": "SlowNitrogenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1290, + "y": 2550 + } + ], + "transitions": [ + { + "id": "transition__s1_draw", + "name": "Draw a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_consumed", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return Math.max(parameters.draw_1 * parameters.draw_enabled, 1e-9);\n});", + "transitionKernelCode": "", + "x": 990, + "y": 1410 + }, + { + "id": "transition__s1_boil_off", + "name": "Boil off a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_evaporated", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.boiloff_rate;\n});", + "transitionKernelCode": "", + "x": 990, + "y": 1020 + }, + { + "id": "transition__s1_raise_order", + "name": "Raise an order, level below trigger (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_permits", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 16, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.review_rate;\n});", + "transitionKernelCode": "", + "x": 2100, + "y": 1575 + }, + { + "id": "transition__s1_dispatch", + "name": "Dispatch a tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.loading_rate;\n});", + "transitionKernelCode": "", + "x": 2580, + "y": 1575 + }, + { + "id": "transition__s1_arrive", + "name": "Unload the tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 12 + }, + { + "placeId": "place__s1_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / (6.0 * parameters.route_scale);\n});", + "transitionKernelCode": "", + "x": 1530, + "y": 1335 + }, + { + "id": "transition__s1_vent", + "name": "Vent through the relief valve (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_vented", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1530, + "y": 1020 + }, + { + "id": "transition__s1_stop_line", + "name": "Stop the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s1_stockouts", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 990, + "y": 1845 + }, + { + "id": "transition__s1_resume_line", + "name": "Resume the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1545, + "y": 2055 + }, + { + "id": "transition__s2_draw", + "name": "Draw a unit (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_consumed", + "weight": 1 + }, + { + "placeId": "place__s2_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return Math.max(parameters.draw_2 * parameters.draw_enabled, 1e-9);\n});", + "transitionKernelCode": "", + "x": 1005, + "y": 2775 + }, + { + "id": "transition__s2_boil_off", + "name": "Boil off a unit (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_evaporated", + "weight": 1 + }, + { + "placeId": "place__s2_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.boiloff_rate;\n});", + "transitionKernelCode": "", + "x": 1005, + "y": 2385 + }, + { + "id": "transition__s2_raise_order", + "name": "Raise an order, level below trigger (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_order_permits", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_contents", + "weight": 6, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_order_placed", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.review_rate;\n});", + "transitionKernelCode": "", + "x": 2130, + "y": 3015 + }, + { + "id": "transition__s2_dispatch", + "name": "Dispatch a tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.loading_rate;\n});", + "transitionKernelCode": "", + "x": 2640, + "y": 3015 + }, + { + "id": "transition__s2_arrive", + "name": "Unload the tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 12 + }, + { + "placeId": "place__s2_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / (9.0 * parameters.route_scale);\n});", + "transitionKernelCode": "", + "x": 1560, + "y": 2700 + }, + { + "id": "transition__s2_vent", + "name": "Vent through the relief valve (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_vented", + "weight": 1 + }, + { + "placeId": "place__s2_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1560, + "y": 2385 + }, + { + "id": "transition__s2_stop_line", + "name": "Stop the line (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s2_stockouts", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1020, + "y": 3165 + }, + { + "id": "transition__s2_resume_line", + "name": "Resume the line (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_line_running", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1560, + "y": 3300 + }, + { + "id": "transition__return_to_depot", + "name": "Return a tanker to the depot", + "inputArcs": [ + { + "placeId": "place__returning", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__idle_tankers", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / parameters.return_time;\n});", + "transitionKernelCode": "", + "x": 2130, + "y": 2145 + } + ], + "types": [], + "differentialEquations": [], + "parameters": [ + { + "id": "param__boiloff_rate", + "name": "Boil-off rate", + "variableName": "boiloff_rate", + "type": "real", + "defaultValue": "0.16" + }, + { + "id": "param__draw_1", + "name": "SteadyNitrogen draw rate", + "variableName": "draw_1", + "type": "real", + "defaultValue": "0.8" + }, + { + "id": "param__draw_2", + "name": "SlowNitrogen draw rate", + "variableName": "draw_2", + "type": "real", + "defaultValue": "0.1" + }, + { + "id": "param__draw_enabled", + "name": "Draw enabled", + "variableName": "draw_enabled", + "type": "real", + "defaultValue": "1" + }, + { + "id": "param__route_scale", + "name": "Route scale", + "variableName": "route_scale", + "type": "real", + "defaultValue": "1" + }, + { + "id": "param__return_time", + "name": "Mean hours on the return leg", + "variableName": "return_time", + "type": "real", + "defaultValue": "4.0" + }, + { + "id": "param__review_rate", + "name": "Telemetry reviews per hour", + "variableName": "review_rate", + "type": "real", + "defaultValue": "4.0" + }, + { + "id": "param__loading_rate", + "name": "Loadings per hour", + "variableName": "loading_rate", + "type": "real", + "defaultValue": "2.0" + }, + { + "id": "param__instant_rate", + "name": "Rate standing for an immediate event", + "variableName": "instant_rate", + "type": "real", + "defaultValue": "20.0" + } + ], + "scenarios": [ + { + "id": "scenario__drawing", + "name": "Customers drawing normally", + "description": "The customers are using product, so consumption events happen and either ordering policy has something to work with.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 1 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n Returning: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: 0,\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n};" + } + }, + { + "id": "scenario__shut", + "name": "Customers shut, tanks still evaporating", + "description": "The customers' plants are down for maintenance and draw nothing. Their tanks still lose product to boil-off. A real operating condition, and where the two ordering policies come apart.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 0 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n Returning: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: 0,\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n};" + } + }, + { + "id": "scenario__two_tankers", + "name": "A second tanker on the depot", + "description": "The same two customers with two trailers instead of one, so neither has to wait for the other's delivery to finish. What contention costs.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 1 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 2,\n LoadsDelivered: 0,\n Returning: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: 0,\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n};" + } + }, + { + "id": "scenario__slow_routes", + "name": "Routes half again as long", + "description": "Winter roads. Every mean journey stretches by half, which lengthens the tail as well as the mean because the journey is exponential.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "draw_enabled", + "default": 1 + }, + { + "type": "real", + "identifier": "route_scale", + "default": 1.5 + } + ], + "parameterOverrides": { + "param__draw_enabled": "scenario.draw_enabled", + "param__route_scale": "scenario.route_scale" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: 1,\n LoadsDelivered: 0,\n Returning: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: 0,\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: 0,\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n};" + } + } + ], + "metrics": [ + { + "id": "metric__deliveries", + "name": "Loads delivered", + "description": "Tanker drops made.", + "code": "return state.places.LoadsDelivered.count;" + }, + { + "id": "metric__stockouts", + "name": "Stockouts", + "description": "Times a customer line stopped for want of product.", + "code": "return state.places.SteadyNitrogenStockouts.count + state.places.SlowNitrogenStockouts.count;" + }, + { + "id": "metric__weighted_stockouts", + "name": "Criticality-weighted stockouts", + "description": "Stockouts weighted by how much the customer matters: the freezing plant counts 2, the laser shop 1. A total count cannot say whether the outages landed on the customer you could least afford to lose.", + "code": "return 2 * state.places.SteadyNitrogenStockouts.count + 1 * state.places.SlowNitrogenStockouts.count;" + }, + { + "id": "metric__vented", + "name": "Vented through relief", + "description": "Units lost through a relief valve, in units at every level. Below the continuous levels the Vented place holds one token per unit, because the tank is a pile of one-unit tokens. From the continuous levels it holds one token per valve opening, each worth vent_loss units, so the count is scaled to keep this metric comparable down the sequence. Structurally reachable everywhere, and unreachable in practice below the continuous levels: under a level trigger the tank never fills completely, and with no pressure nothing else opens the valve.", + "code": "return state.places.SteadyNitrogenVented.count + state.places.SlowNitrogenVented.count;" + }, + { + "id": "metric__evaporated", + "name": "Evaporated", + "description": "Units lost to boil-off.", + "code": "return state.places.SteadyNitrogenEvaporated.count + state.places.SlowNitrogenEvaporated.count;" + }, + { + "id": "metric__consumed", + "name": "Consumed", + "description": "Units the customers actually used.", + "code": "return state.places.SteadyNitrogenConsumed.count + state.places.SlowNitrogenConsumed.count;" + }, + { + "id": "metric__boiloff_share", + "name": "Share of outflow lost to boil-off", + "description": "Evaporated over everything that left the tanks. The quantity a consumption trigger is blind to, as a fraction.", + "code": "const consumed = state.places.SteadyNitrogenConsumed.count + state.places.SlowNitrogenConsumed.count;\nconst evaporated = state.places.SteadyNitrogenEvaporated.count + state.places.SlowNitrogenEvaporated.count;\nreturn consumed + evaporated > 0 ? evaporated / (consumed + evaporated) : 0;" + }, + { + "id": "metric__stockouts_per_hundred", + "name": "Stockouts per 100 units consumed", + "description": "Stockouts against the volume the customers actually drew. Safe to compare across this level's scenarios, which a raw count is not, because a scenario that delivers more has more chances to fail.", + "code": "const consumed = state.places.SteadyNitrogenConsumed.count + state.places.SlowNitrogenConsumed.count;\nreturn consumed > 0 ? 100 * (state.places.SteadyNitrogenStockouts.count + state.places.SlowNitrogenStockouts.count) / consumed : 0;" + }, + { + "id": "metric__stockouts_1", + "name": "SteadyNitrogen stockouts", + "description": "Times the food freezing plant stopped.", + "code": "return state.places.SteadyNitrogenStockouts.count;" + }, + { + "id": "metric__stockouts_2", + "name": "SlowNitrogen stockouts", + "description": "Times the laser cutting shop stopped.", + "code": "return state.places.SlowNitrogenStockouts.count;" + }, + { + "id": "metric__envelope", + "name": "Contents plus ullage", + "description": "The place invariant, summed over both sites. Every transition that moves product moves it between Contents and Ullage, so this is total capacity in every reachable marking and can be checked without simulating anything.", + "code": "return state.places.SteadyNitrogenContents.count + state.places.SteadyNitrogenUllage.count + state.places.SlowNitrogenContents.count + state.places.SlowNitrogenUllage.count;" + }, + { + "id": "metric__stranded", + "name": "Stranded customers", + "description": "Customers that ended stopped with no order outstanding: the line is down and nothing is on its way to fix it.", + "code": "return (state.places.SteadyNitrogenLineStopped.count > 0 && state.places.SteadyNitrogenOrderPlaced.count === 0 ? 1 : 0) + (state.places.SlowNitrogenLineStopped.count > 0 && state.places.SlowNitrogenOrderPlaced.count === 0 ? 1 : 0);" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Gases 2 β€” stochastic net, two customers on one tanker" +} diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-3-cpn-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-3-cpn-layout.json new file mode 100644 index 00000000000..6c0226d031e --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-3-cpn-layout.json @@ -0,0 +1,1300 @@ +{ + "places": [ + { + "id": "place__idle_tankers", + "name": "IdleTankers", + "colorId": "type__tanker", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2535, + "y": 1830 + }, + { + "id": "place__loads_delivered", + "name": "LoadsDelivered", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2085, + "y": 2115 + }, + { + "id": "place__returning", + "name": "Returning", + "colorId": "type__tanker", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2085, + "y": 1830 + }, + { + "id": "place__s1_order_placed", + "name": "SteadyNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2235, + "y": 2340 + }, + { + "id": "place__s1_order_permits", + "name": "SteadyNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1785, + "y": 2220 + }, + { + "id": "place__s1_on_route", + "name": "SteadyNitrogenOnRoute", + "colorId": "type__tanker", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2655, + "y": 2340 + }, + { + "id": "place__s1_vented", + "name": "SteadyNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1785, + "y": 1770 + }, + { + "id": "place__s1_line_running", + "name": "SteadyNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 645, + "y": 2625 + }, + { + "id": "place__s1_line_stopped", + "name": "SteadyNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 2715 + }, + { + "id": "place__s1_stockouts", + "name": "SteadyNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 2490 + }, + { + "id": "place__s1_consumed", + "name": "SteadyNitrogenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 2325 + }, + { + "id": "place__s1_evaporated", + "name": "SteadyNitrogenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 1665 + }, + { + "id": "place__s1_contents", + "name": "SteadyNitrogenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 645, + "y": 2070 + }, + { + "id": "place__s1_ullage", + "name": "SteadyNitrogenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1245, + "y": 1950 + }, + { + "id": "place__s2_order_placed", + "name": "SlowNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2280, + "y": 3585 + }, + { + "id": "place__s2_order_permits", + "name": "SlowNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1800, + "y": 3465 + }, + { + "id": "place__s2_on_route", + "name": "SlowNitrogenOnRoute", + "colorId": "type__tanker", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2715, + "y": 3585 + }, + { + "id": "place__s2_vented", + "name": "SlowNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1740, + "y": 2985 + }, + { + "id": "place__s2_line_running", + "name": "SlowNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 675, + "y": 3795 + }, + { + "id": "place__s2_line_stopped", + "name": "SlowNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1260, + "y": 3930 + }, + { + "id": "place__s2_stockouts", + "name": "SlowNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 3675 + }, + { + "id": "place__s2_consumed", + "name": "SlowNitrogenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 3495 + }, + { + "id": "place__s2_evaporated", + "name": "SlowNitrogenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 2895 + }, + { + "id": "place__s2_contents", + "name": "SlowNitrogenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 660, + "y": 3210 + }, + { + "id": "place__s2_ullage", + "name": "SlowNitrogenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1245, + "y": 3165 + }, + { + "id": "place__s3_order_placed", + "name": "CriticalOxygenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2280, + "y": 1110 + }, + { + "id": "place__s3_order_permits", + "name": "CriticalOxygenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1785, + "y": 975 + }, + { + "id": "place__s3_on_route", + "name": "CriticalOxygenOnRoute", + "colorId": "type__tanker", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2745, + "y": 1110 + }, + { + "id": "place__s3_vented", + "name": "CriticalOxygenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1800, + "y": 555 + }, + { + "id": "place__s3_line_running", + "name": "CriticalOxygenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 615, + "y": 1350 + }, + { + "id": "place__s3_line_stopped", + "name": "CriticalOxygenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 1485 + }, + { + "id": "place__s3_stockouts", + "name": "CriticalOxygenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 1200 + }, + { + "id": "place__s3_consumed", + "name": "CriticalOxygenConsumed", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 1050 + }, + { + "id": "place__s3_evaporated", + "name": "CriticalOxygenEvaporated", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1245, + "y": 450 + }, + { + "id": "place__s3_contents", + "name": "CriticalOxygenContents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 615, + "y": 795 + }, + { + "id": "place__s3_ullage", + "name": "CriticalOxygenUllage", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1230, + "y": 735 + } + ], + "transitions": [ + { + "id": "transition__s1_draw", + "name": "Draw a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_consumed", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.draw_1;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 2295 + }, + { + "id": "transition__s1_boil_off", + "name": "Boil off a unit (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_evaporated", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.boiloff_rate;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 1815 + }, + { + "id": "transition__s1_raise_order", + "name": "Raise an order, level below trigger (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_permits", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 16, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.review_rate;\n});", + "transitionKernelCode": "", + "x": 2025, + "y": 2340 + }, + { + "id": "transition__s1_dispatch", + "name": "Dispatch a tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.IdleTankers[0].product === \"nitrogen\" ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n return { SteadyNitrogenOnRoute: [{ product: input.IdleTankers[0].product }] };\n});", + "x": 2445, + "y": 2340 + }, + { + "id": "transition__s1_arrive", + "name": "Unload the tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 12 + }, + { + "placeId": "place__s1_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / (6.0 * parameters.route_scale);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n Returning: [{ product: input.SteadyNitrogenOnRoute[0].product }],\n };\n});", + "x": 1485, + "y": 2115 + }, + { + "id": "transition__s1_vent", + "name": "Vent through the relief valve (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_vented", + "weight": 1 + }, + { + "placeId": "place__s1_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1485, + "y": 1770 + }, + { + "id": "transition__s1_stop_line", + "name": "Stop the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s1_stockouts", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 2625 + }, + { + "id": "transition__s1_resume_line", + "name": "Resume the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_line_running", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1470, + "y": 2715 + }, + { + "id": "transition__s2_draw", + "name": "Draw a unit (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_consumed", + "weight": 1 + }, + { + "placeId": "place__s2_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.draw_2;\n});", + "transitionKernelCode": "", + "x": 975, + "y": 3435 + }, + { + "id": "transition__s2_boil_off", + "name": "Boil off a unit (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_evaporated", + "weight": 1 + }, + { + "placeId": "place__s2_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.boiloff_rate;\n});", + "transitionKernelCode": "", + "x": 975, + "y": 3015 + }, + { + "id": "transition__s2_raise_order", + "name": "Raise an order, level below trigger (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_order_permits", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_contents", + "weight": 6, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_order_placed", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.review_rate;\n});", + "transitionKernelCode": "", + "x": 2040, + "y": 3585 + }, + { + "id": "transition__s2_dispatch", + "name": "Dispatch a tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.IdleTankers[0].product === \"nitrogen\" ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n return { SlowNitrogenOnRoute: [{ product: input.IdleTankers[0].product }] };\n});", + "x": 2505, + "y": 3585 + }, + { + "id": "transition__s2_arrive", + "name": "Unload the tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 12 + }, + { + "placeId": "place__s2_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / (9.0 * parameters.route_scale);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n Returning: [{ product: input.SlowNitrogenOnRoute[0].product }],\n };\n});", + "x": 1530, + "y": 3330 + }, + { + "id": "transition__s2_vent", + "name": "Vent through the relief valve (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_vented", + "weight": 1 + }, + { + "placeId": "place__s2_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1515, + "y": 2985 + }, + { + "id": "transition__s2_stop_line", + "name": "Stop the line (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s2_stockouts", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 975, + "y": 3795 + }, + { + "id": "transition__s2_resume_line", + "name": "Resume the line (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_line_running", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1530, + "y": 3930 + }, + { + "id": "transition__s3_draw", + "name": "Draw a unit (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_consumed", + "weight": 1 + }, + { + "placeId": "place__s3_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.draw_3;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 1020 + }, + { + "id": "transition__s3_boil_off", + "name": "Boil off a unit (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_contents", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_evaporated", + "weight": 1 + }, + { + "placeId": "place__s3_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.boiloff_rate;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 540 + }, + { + "id": "transition__s3_raise_order", + "name": "Raise an order, level below trigger (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_order_permits", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_contents", + "weight": 20, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_order_placed", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.review_rate;\n});", + "transitionKernelCode": "", + "x": 2055, + "y": 1110 + }, + { + "id": "transition__s3_dispatch", + "name": "Dispatch a tanker (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.IdleTankers[0].product === \"oxygen\" ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n return { CriticalOxygenOnRoute: [{ product: input.IdleTankers[0].product }] };\n});", + "x": 2505, + "y": 1110 + }, + { + "id": "transition__s3_arrive", + "name": "Unload the tanker (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_ullage", + "weight": 12, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_contents", + "weight": 12 + }, + { + "placeId": "place__s3_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / (12.0 * parameters.route_scale);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n Returning: [{ product: input.CriticalOxygenOnRoute[0].product }],\n };\n});", + "x": 1485, + "y": 870 + }, + { + "id": "transition__s3_vent", + "name": "Vent through the relief valve (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_contents", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_ullage", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_vented", + "weight": 1 + }, + { + "placeId": "place__s3_ullage", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1500, + "y": 555 + }, + { + "id": "transition__s3_stop_line", + "name": "Stop the line (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_line_running", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_contents", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s3_stockouts", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 960, + "y": 1350 + }, + { + "id": "transition__s3_resume_line", + "name": "Resume the line (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_line_stopped", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_contents", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_line_running", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.instant_rate;\n});", + "transitionKernelCode": "", + "x": 1485, + "y": 1485 + }, + { + "id": "transition__return_to_depot", + "name": "Return a tanker to the depot", + "inputArcs": [ + { + "placeId": "place__returning", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__idle_tankers", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / parameters.return_time;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n return { IdleTankers: [{ product: input.Returning[0].product }] };\n});", + "x": 2295, + "y": 1830 + } + ], + "types": [ + { + "id": "type__tanker", + "name": "Tanker", + "iconSlug": "circle", + "displayColor": "#0ea5e9", + "elements": [ + { + "elementId": "type__tanker__product", + "name": "product", + "type": "string" + } + ] + } + ], + "differentialEquations": [], + "parameters": [ + { + "id": "param__boiloff_rate", + "name": "Boil-off rate", + "variableName": "boiloff_rate", + "type": "real", + "defaultValue": "0.16" + }, + { + "id": "param__draw_1", + "name": "SteadyNitrogen draw rate", + "variableName": "draw_1", + "type": "real", + "defaultValue": "0.8" + }, + { + "id": "param__draw_2", + "name": "SlowNitrogen draw rate", + "variableName": "draw_2", + "type": "real", + "defaultValue": "0.1" + }, + { + "id": "param__draw_3", + "name": "CriticalOxygen draw rate", + "variableName": "draw_3", + "type": "real", + "defaultValue": "0.6" + }, + { + "id": "param__route_scale", + "name": "Route scale", + "variableName": "route_scale", + "type": "real", + "defaultValue": "1" + }, + { + "id": "param__return_time", + "name": "Return leg (hours)", + "variableName": "return_time", + "type": "real", + "defaultValue": "4.0" + }, + { + "id": "param__instant_rate", + "name": "Instant rate (pseudo-immediate)", + "variableName": "instant_rate", + "type": "real", + "defaultValue": "1000" + }, + { + "id": "param__review_rate", + "name": "Order review rate", + "variableName": "review_rate", + "type": "real", + "defaultValue": "10" + }, + { + "id": "param__loading_rate", + "name": "Depot loading rate", + "variableName": "loading_rate", + "type": "real", + "defaultValue": "4" + } + ], + "scenarios": [ + { + "id": "scenario__base", + "name": "Three tankers, normal routes", + "description": "The reference case: three tankers on the depot, routes at their nominal length.", + "scenarioParameters": [], + "parameterOverrides": {}, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ product: \"nitrogen\" }, { product: \"nitrogen\" }, { product: \"oxygen\" }],\n LoadsDelivered: 0,\n Returning: [],\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenConsumed: 0,\n CriticalOxygenEvaporated: 0,\n CriticalOxygenContents: 46,\n CriticalOxygenUllage: 12,\n};" + } + }, + { + "id": "scenario__slow_routes", + "name": "Three tankers, routes half again as long", + "description": "Winter roads. Every route stretches by half, which is a question about the tail of the delay rather than its mean.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "route_scale", + "default": 1.5 + } + ], + "parameterOverrides": { + "param__route_scale": "scenario.route_scale" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ product: \"nitrogen\" }, { product: \"nitrogen\" }, { product: \"oxygen\" }],\n LoadsDelivered: 0,\n Returning: [],\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenConsumed: 0,\n CriticalOxygenEvaporated: 0,\n CriticalOxygenContents: 46,\n CriticalOxygenUllage: 12,\n};" + } + }, + { + "id": "scenario__two_tankers", + "name": "Two tankers, normal routes", + "description": "One trailer off the road. What the fleet can absorb.", + "scenarioParameters": [], + "parameterOverrides": {}, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ product: \"nitrogen\" }, { product: \"oxygen\" }],\n LoadsDelivered: 0,\n Returning: [],\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenConsumed: 0,\n CriticalOxygenEvaporated: 0,\n CriticalOxygenContents: 46,\n CriticalOxygenUllage: 12,\n};" + } + }, + { + "id": "scenario__second_oxygen_tanker", + "name": "Three tankers, two of them oxygen", + "description": "The same fleet size, re-specified so two trailers can serve the metals plant. Only a coloured net can tell this apart from the base case.", + "scenarioParameters": [], + "parameterOverrides": {}, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ product: \"nitrogen\" }, { product: \"oxygen\" }, { product: \"oxygen\" }],\n LoadsDelivered: 0,\n Returning: [],\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenConsumed: 0,\n SteadyNitrogenEvaporated: 0,\n SteadyNitrogenContents: 42,\n SteadyNitrogenUllage: 12,\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenConsumed: 0,\n SlowNitrogenEvaporated: 0,\n SlowNitrogenContents: 18,\n SlowNitrogenUllage: 12,\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenConsumed: 0,\n CriticalOxygenEvaporated: 0,\n CriticalOxygenContents: 46,\n CriticalOxygenUllage: 12,\n};" + } + } + ], + "metrics": [ + { + "id": "metric__deliveries", + "name": "Loads delivered", + "description": "Tanker drops made.", + "code": "return state.places.LoadsDelivered.count;" + }, + { + "id": "metric__stockouts", + "name": "Stockouts", + "description": "Times a customer line stopped for want of product.", + "code": "return state.places.SteadyNitrogenStockouts.count + state.places.SlowNitrogenStockouts.count + state.places.CriticalOxygenStockouts.count;" + }, + { + "id": "metric__weighted_stockouts", + "name": "Criticality-weighted stockouts", + "description": "Stockouts weighted by how much the customer matters: the metals plant counts 3, the freezing plant 2, the laser shop 1. A total count cannot say whether the outages landed on the customer you could least afford to lose.", + "code": "return 2 * state.places.SteadyNitrogenStockouts.count + 1 * state.places.SlowNitrogenStockouts.count + 3 * state.places.CriticalOxygenStockouts.count;" + }, + { + "id": "metric__vented", + "name": "Vented through relief", + "description": "Units lost through a relief valve, in units at every level. Below the continuous levels the Vented place holds one token per unit, because the tank is a pile of one-unit tokens. From the continuous levels it holds one token per valve opening, each worth vent_loss units, so the count is scaled to keep this metric comparable down the sequence. Structurally reachable everywhere, and unreachable in practice below the continuous levels: under a level trigger the tank never fills completely, and with no pressure nothing else opens the valve.", + "code": "return state.places.SteadyNitrogenVented.count + state.places.SlowNitrogenVented.count + state.places.CriticalOxygenVented.count;" + }, + { + "id": "metric__evaporated", + "name": "Evaporated", + "description": "Units lost to boil-off.", + "code": "return state.places.SteadyNitrogenEvaporated.count + state.places.SlowNitrogenEvaporated.count + state.places.CriticalOxygenEvaporated.count;" + }, + { + "id": "metric__consumed", + "name": "Consumed", + "description": "Units the customers actually used.", + "code": "return state.places.SteadyNitrogenConsumed.count + state.places.SlowNitrogenConsumed.count + state.places.CriticalOxygenConsumed.count;" + }, + { + "id": "metric__stockouts_1", + "name": "SteadyNitrogen stockouts", + "description": "Times the food freezing plant stopped.", + "code": "return state.places.SteadyNitrogenStockouts.count;" + }, + { + "id": "metric__stockouts_2", + "name": "SlowNitrogen stockouts", + "description": "Times the laser cutting shop stopped.", + "code": "return state.places.SlowNitrogenStockouts.count;" + }, + { + "id": "metric__stockouts_3", + "name": "CriticalOxygen stockouts", + "description": "Times the metals plant stopped.", + "code": "return state.places.CriticalOxygenStockouts.count;" + }, + { + "id": "metric__envelope", + "name": "Contents plus ullage", + "description": "The place invariant, summed over the three sites. Every transition that moves product moves it between Contents and Ullage, so this is total capacity in every reachable marking and can be checked without simulating anything.", + "code": "return state.places.SteadyNitrogenContents.count + state.places.SteadyNitrogenUllage.count + state.places.SlowNitrogenContents.count + state.places.SlowNitrogenUllage.count + state.places.CriticalOxygenContents.count + state.places.CriticalOxygenUllage.count;" + }, + { + "id": "metric__stranded", + "name": "Stranded customers", + "description": "Customers that ended stopped with no order outstanding: the line is down and nothing is on its way to fix it.", + "code": "return (state.places.SteadyNitrogenLineStopped.count > 0 && state.places.SteadyNitrogenOrderPlaced.count === 0 ? 1 : 0) + (state.places.SlowNitrogenLineStopped.count > 0 && state.places.SlowNitrogenOrderPlaced.count === 0 ? 1 : 0) + (state.places.CriticalOxygenLineStopped.count > 0 && state.places.CriticalOxygenOrderPlaced.count === 0 ? 1 : 0);" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Gases 3 \u2014 coloured net, three customers and a mixed fleet" +} diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-4-dcpn-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-4-dcpn-layout.json new file mode 100644 index 00000000000..cf0b2b1947f --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-4-dcpn-layout.json @@ -0,0 +1,1619 @@ +{ + "places": [ + { + "id": "place__idle_tankers", + "name": "IdleTankers", + "colorId": "type__tanker", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2190, + "y": 1875 + }, + { + "id": "place__loads_delivered", + "name": "LoadsDelivered", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1890, + "y": 2100 + }, + { + "id": "place__returning", + "name": "Returning", + "colorId": "type__tanker", + "dynamicsEnabled": true, + "differentialEquationId": "de__returning", + "showAsInitialState": false, + "x": 1725, + "y": 1875 + }, + { + "id": "place__open_orders", + "name": "OpenOrders", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2160, + "y": 2580 + }, + { + "id": "place__hires", + "name": "Hires", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2880, + "y": 2010 + }, + { + "id": "place__plant", + "name": "Plant", + "colorId": "type__plant", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1755, + "y": 2565 + }, + { + "id": "place__outages", + "name": "Outages", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2115, + "y": 2760 + }, + { + "id": "place__s1_order_placed", + "name": "SteadyNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2100, + "y": 2325 + }, + { + "id": "place__s1_order_permits", + "name": "SteadyNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1545, + "y": 2190 + }, + { + "id": "place__s1_on_route", + "name": "SteadyNitrogenOnRoute", + "colorId": "type__tanker", + "dynamicsEnabled": true, + "differentialEquationId": "de__on_route", + "showAsInitialState": false, + "x": 2940, + "y": 2310 + }, + { + "id": "place__s1_vented", + "name": "SteadyNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1440, + "y": 1845 + }, + { + "id": "place__s1_line_running", + "name": "SteadyNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 615, + "y": 2565 + }, + { + "id": "place__s1_line_stopped", + "name": "SteadyNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1170, + "y": 2700 + }, + { + "id": "place__s1_stockouts", + "name": "SteadyNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1170, + "y": 2415 + }, + { + "id": "place__s1_tank", + "name": "SteadyNitrogenTank", + "colorId": "type__tank", + "dynamicsEnabled": true, + "differentialEquationId": "de__tank", + "showAsInitialState": true, + "x": 615, + "y": 2010 + }, + { + "id": "place__s2_order_placed", + "name": "SlowNitrogenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2100, + "y": 3540 + }, + { + "id": "place__s2_order_permits", + "name": "SlowNitrogenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1530, + "y": 3375 + }, + { + "id": "place__s2_on_route", + "name": "SlowNitrogenOnRoute", + "colorId": "type__tanker", + "dynamicsEnabled": true, + "differentialEquationId": "de__on_route", + "showAsInitialState": false, + "x": 2955, + "y": 3540 + }, + { + "id": "place__s2_vented", + "name": "SlowNitrogenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1455, + "y": 2970 + }, + { + "id": "place__s2_line_running", + "name": "SlowNitrogenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 600, + "y": 3720 + }, + { + "id": "place__s2_line_stopped", + "name": "SlowNitrogenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1185, + "y": 3855 + }, + { + "id": "place__s2_stockouts", + "name": "SlowNitrogenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1185, + "y": 3570 + }, + { + "id": "place__s2_tank", + "name": "SlowNitrogenTank", + "colorId": "type__tank", + "dynamicsEnabled": true, + "differentialEquationId": "de__tank", + "showAsInitialState": true, + "x": 585, + "y": 3165 + }, + { + "id": "place__s3_order_placed", + "name": "CriticalOxygenOrderPlaced", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2115, + "y": 1350 + }, + { + "id": "place__s3_order_permits", + "name": "CriticalOxygenOrderPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1545, + "y": 1245 + }, + { + "id": "place__s3_on_route", + "name": "CriticalOxygenOnRoute", + "colorId": "type__tanker", + "dynamicsEnabled": true, + "differentialEquationId": "de__on_route", + "showAsInitialState": false, + "x": 2955, + "y": 1335 + }, + { + "id": "place__s3_vented", + "name": "CriticalOxygenVented", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1455, + "y": 780 + }, + { + "id": "place__s3_line_running", + "name": "CriticalOxygenLineRunning", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 615, + "y": 1440 + }, + { + "id": "place__s3_line_stopped", + "name": "CriticalOxygenLineStopped", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1170, + "y": 1575 + }, + { + "id": "place__s3_stockouts", + "name": "CriticalOxygenStockouts", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1170, + "y": 1290 + }, + { + "id": "place__s3_tank", + "name": "CriticalOxygenTank", + "colorId": "type__tank", + "dynamicsEnabled": true, + "differentialEquationId": "de__tank", + "showAsInitialState": true, + "x": 645, + "y": 960 + } + ], + "transitions": [ + { + "id": "transition__s1_raise_order", + "name": "Raise an order (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_order_permits", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1 + }, + { + "placeId": "place__s1_order_placed", + "weight": 1 + }, + { + "placeId": "place__open_orders", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// The same threshold the plain net wrote as an inhibitor arc, now reading a\n// continuous level. Crossing it forces an order: a boundary jump, not a poll.\nexport default Lambda((input, parameters) => {\n return input.SteadyNitrogenTank[0].level < parameters.trigger_1;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.SteadyNitrogenTank[0];\n return { SteadyNitrogenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 1845, + "y": 2325 + }, + { + "id": "transition__s1_dispatch", + "name": "Dispatch a tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__plant", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return (input.IdleTankers[0].product === \"nitrogen\" && input.Plant[0].up === 1) ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.IdleTankers[0];\n return {\n SteadyNitrogenOnRoute: [{ remaining: Distribution.Lognormal(Math.log(6.0 * parameters.route_scale), parameters.route_spread), product: unit.product, payload: unit.payload, hired: unit.hired }],\n };\n});", + "x": 2685, + "y": 2190 + }, + { + "id": "transition__s1_dispatch_resourced", + "name": "Dispatch a re-sourced tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__plant", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return (input.IdleTankers[0].product === \"nitrogen\" && input.Plant[0].up === 0) ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.IdleTankers[0];\n return {\n SteadyNitrogenOnRoute: [{ remaining: Distribution.Lognormal(Math.log(6.0 * parameters.route_scale * parameters.outage_route_penalty), parameters.route_spread), product: unit.product, payload: unit.payload, hired: unit.hired }],\n };\n});", + "x": 2685, + "y": 2415 + }, + { + "id": "transition__s1_arrive", + "name": "Unload the tanker (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_tank", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1 + }, + { + "placeId": "place__s1_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.SteadyNitrogenOnRoute[0].remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.SteadyNitrogenOnRoute[0];\n const unit = input.SteadyNitrogenTank[0];\n const taken = Math.min(truck.payload, Math.max(unit.capacity - unit.level, 0));\n return {\n SteadyNitrogenTank: [\n {\n level: unit.level + taken, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled + (truck.payload - taken)\n },\n ],\n Returning: [\n { remaining: parameters.return_time, product: truck.product, payload: truck.payload, hired: truck.hired },\n ],\n };\n});", + "x": 1170, + "y": 2100 + }, + { + "id": "transition__s1_vent", + "name": "Vent through the relief valve (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1 + }, + { + "placeId": "place__s1_vented", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// The boundary jump no lower rung can express. Pressure reaching the setpoint\n// forces a discrete loss of product, and the valve reseats below the setpoint\n// so it cycles rather than firing once.\nexport default Lambda((input, parameters) => {\n return input.SteadyNitrogenTank[0].pressure >= parameters.relief_setpoint;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.SteadyNitrogenTank[0];\n return {\n SteadyNitrogenTank: [\n {\n level: Math.max(unit.level - parameters.vent_loss, 0), pressure: parameters.relief_setpoint - parameters.relief_reseat, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled\n },\n ],\n };\n});", + "x": 1170, + "y": 1845 + }, + { + "id": "transition__s1_stop_line", + "name": "Stop the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_line_running", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1 + }, + { + "placeId": "place__s1_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s1_stockouts", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.SteadyNitrogenTank[0].level <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.SteadyNitrogenTank[0];\n return { SteadyNitrogenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 885, + "y": 2565 + }, + { + "id": "transition__s1_resume_line", + "name": "Resume the line (SteadyNitrogen)", + "inputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s1_line_stopped", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s1_tank", + "weight": 1 + }, + { + "placeId": "place__s1_line_running", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.SteadyNitrogenTank[0].level > 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.SteadyNitrogenTank[0];\n return { SteadyNitrogenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 1410, + "y": 2700 + }, + { + "id": "transition__s2_raise_order", + "name": "Raise an order (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_order_permits", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1 + }, + { + "placeId": "place__s2_order_placed", + "weight": 1 + }, + { + "placeId": "place__open_orders", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// The same threshold the plain net wrote as an inhibitor arc, now reading a\n// continuous level. Crossing it forces an order: a boundary jump, not a poll.\nexport default Lambda((input, parameters) => {\n return input.SlowNitrogenTank[0].level < parameters.trigger_2;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.SlowNitrogenTank[0];\n return { SlowNitrogenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 1830, + "y": 3540 + }, + { + "id": "transition__s2_dispatch", + "name": "Dispatch a tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__plant", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return (input.IdleTankers[0].product === \"nitrogen\" && input.Plant[0].up === 1) ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.IdleTankers[0];\n return {\n SlowNitrogenOnRoute: [{ remaining: Distribution.Lognormal(Math.log(9.0 * parameters.route_scale), parameters.route_spread), product: unit.product, payload: unit.payload, hired: unit.hired }],\n };\n});", + "x": 2670, + "y": 3450 + }, + { + "id": "transition__s2_dispatch_resourced", + "name": "Dispatch a re-sourced tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__plant", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return (input.IdleTankers[0].product === \"nitrogen\" && input.Plant[0].up === 0) ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.IdleTankers[0];\n return {\n SlowNitrogenOnRoute: [{ remaining: Distribution.Lognormal(Math.log(9.0 * parameters.route_scale * parameters.outage_route_penalty), parameters.route_spread), product: unit.product, payload: unit.payload, hired: unit.hired }],\n };\n});", + "x": 2670, + "y": 3630 + }, + { + "id": "transition__s2_arrive", + "name": "Unload the tanker (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_tank", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1 + }, + { + "placeId": "place__s2_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.SlowNitrogenOnRoute[0].remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.SlowNitrogenOnRoute[0];\n const unit = input.SlowNitrogenTank[0];\n const taken = Math.min(truck.payload, Math.max(unit.capacity - unit.level, 0));\n return {\n SlowNitrogenTank: [\n {\n level: unit.level + taken, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled + (truck.payload - taken)\n },\n ],\n Returning: [\n { remaining: parameters.return_time, product: truck.product, payload: truck.payload, hired: truck.hired },\n ],\n };\n});", + "x": 1170, + "y": 3240 + }, + { + "id": "transition__s2_vent", + "name": "Vent through the relief valve (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1 + }, + { + "placeId": "place__s2_vented", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// The boundary jump no lower rung can express. Pressure reaching the setpoint\n// forces a discrete loss of product, and the valve reseats below the setpoint\n// so it cycles rather than firing once.\nexport default Lambda((input, parameters) => {\n return input.SlowNitrogenTank[0].pressure >= parameters.relief_setpoint;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.SlowNitrogenTank[0];\n return {\n SlowNitrogenTank: [\n {\n level: Math.max(unit.level - parameters.vent_loss, 0), pressure: parameters.relief_setpoint - parameters.relief_reseat, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled\n },\n ],\n };\n});", + "x": 1170, + "y": 2970 + }, + { + "id": "transition__s2_stop_line", + "name": "Stop the line (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_line_running", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1 + }, + { + "placeId": "place__s2_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s2_stockouts", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.SlowNitrogenTank[0].level <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.SlowNitrogenTank[0];\n return { SlowNitrogenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 870, + "y": 3720 + }, + { + "id": "transition__s2_resume_line", + "name": "Resume the line (SlowNitrogen)", + "inputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s2_line_stopped", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s2_tank", + "weight": 1 + }, + { + "placeId": "place__s2_line_running", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.SlowNitrogenTank[0].level > 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.SlowNitrogenTank[0];\n return { SlowNitrogenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 1425, + "y": 3855 + }, + { + "id": "transition__s3_raise_order", + "name": "Raise an order (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_order_permits", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1 + }, + { + "placeId": "place__s3_order_placed", + "weight": 1 + }, + { + "placeId": "place__open_orders", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// The same threshold the plain net wrote as an inhibitor arc, now reading a\n// continuous level. Crossing it forces an order: a boundary jump, not a poll.\nexport default Lambda((input, parameters) => {\n return input.CriticalOxygenTank[0].level < parameters.trigger_3;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.CriticalOxygenTank[0];\n return { CriticalOxygenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 1845, + "y": 1350 + }, + { + "id": "transition__s3_dispatch", + "name": "Dispatch a tanker (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__plant", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return (input.IdleTankers[0].product === \"oxygen\" && input.Plant[0].up === 1) ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.IdleTankers[0];\n return {\n CriticalOxygenOnRoute: [{ remaining: Distribution.Lognormal(Math.log(12.0 * parameters.route_scale), parameters.route_spread), product: unit.product, payload: unit.payload, hired: unit.hired }],\n };\n});", + "x": 2700, + "y": 1260 + }, + { + "id": "transition__s3_dispatch_resourced", + "name": "Dispatch a re-sourced tanker (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_order_placed", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__plant", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_on_route", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return (input.IdleTankers[0].product === \"oxygen\" && input.Plant[0].up === 0) ? parameters.loading_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.IdleTankers[0];\n return {\n CriticalOxygenOnRoute: [{ remaining: Distribution.Lognormal(Math.log(12.0 * parameters.route_scale * parameters.outage_route_penalty), parameters.route_spread), product: unit.product, payload: unit.payload, hired: unit.hired }],\n };\n});", + "x": 2715, + "y": 1440 + }, + { + "id": "transition__s3_arrive", + "name": "Unload the tanker (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_on_route", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_tank", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1 + }, + { + "placeId": "place__s3_order_permits", + "weight": 1 + }, + { + "placeId": "place__loads_delivered", + "weight": 1 + }, + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.CriticalOxygenOnRoute[0].remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.CriticalOxygenOnRoute[0];\n const unit = input.CriticalOxygenTank[0];\n const taken = Math.min(truck.payload, Math.max(unit.capacity - unit.level, 0));\n return {\n CriticalOxygenTank: [\n {\n level: unit.level + taken, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled + (truck.payload - taken)\n },\n ],\n Returning: [\n { remaining: parameters.return_time, product: truck.product, payload: truck.payload, hired: truck.hired },\n ],\n };\n});", + "x": 1170, + "y": 1080 + }, + { + "id": "transition__s3_vent", + "name": "Vent through the relief valve (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1 + }, + { + "placeId": "place__s3_vented", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "// The boundary jump no lower rung can express. Pressure reaching the setpoint\n// forces a discrete loss of product, and the valve reseats below the setpoint\n// so it cycles rather than firing once.\nexport default Lambda((input, parameters) => {\n return input.CriticalOxygenTank[0].pressure >= parameters.relief_setpoint;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const unit = input.CriticalOxygenTank[0];\n return {\n CriticalOxygenTank: [\n {\n level: Math.max(unit.level - parameters.vent_loss, 0), pressure: parameters.relief_setpoint - parameters.relief_reseat, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled\n },\n ],\n };\n});", + "x": 1170, + "y": 780 + }, + { + "id": "transition__s3_stop_line", + "name": "Stop the line (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_line_running", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1 + }, + { + "placeId": "place__s3_line_stopped", + "weight": 1 + }, + { + "placeId": "place__s3_stockouts", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.CriticalOxygenTank[0].level <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.CriticalOxygenTank[0];\n return { CriticalOxygenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 885, + "y": 1440 + }, + { + "id": "transition__s3_resume_line", + "name": "Resume the line (CriticalOxygen)", + "inputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__s3_line_stopped", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__s3_tank", + "weight": 1 + }, + { + "placeId": "place__s3_line_running", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.CriticalOxygenTank[0].level > 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.CriticalOxygenTank[0];\n return { CriticalOxygenTank: [{ level: unit.level, pressure: unit.pressure, capacity: unit.capacity, draw: unit.draw, drawn: unit.drawn, boiled: unit.boiled, criticality: unit.criticality, product: unit.product, spilled: unit.spilled }] };\n});", + "x": 1425, + "y": 1575 + }, + { + "id": "transition__return_to_depot", + "name": "Return a tanker to the depot", + "inputArcs": [ + { + "placeId": "place__returning", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__idle_tankers", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.Returning[0].remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const unit = input.Returning[0];\n return { IdleTankers: [{ remaining: 0, product: unit.product, payload: unit.payload, hired: unit.hired }] };\n});", + "x": 1950, + "y": 1875 + }, + { + "id": "transition__hire_tanker", + "name": "Hire a tanker", + "inputArcs": [ + { + "placeId": "place__open_orders", + "weight": 3, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__idle_tankers", + "weight": 1 + }, + { + "placeId": "place__hires", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.hire_enabled > 0 ? parameters.hire_rate : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n IdleTankers: [\n {\n remaining: 0,\n product: parameters.hire_oxygen > 0 ? \"oxygen\" : \"nitrogen\",\n payload: parameters.hired_payload,\n hired: 1,\n },\n ],\n };\n});", + "x": 2580, + "y": 2010 + }, + { + "id": "transition__release_tanker", + "name": "Release a hired tanker", + "inputArcs": [ + { + "placeId": "place__idle_tankers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__open_orders", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.IdleTankers[0].hired === 1 ? 1000 : 1e-9;\n});", + "transitionKernelCode": "", + "x": 2565, + "y": 2580 + }, + { + "id": "transition__plant_trips", + "name": "Trip the air separation plant", + "inputArcs": [ + { + "placeId": "place__plant", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__plant", + "weight": 1 + }, + { + "placeId": "place__outages", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.Plant[0].up === 1 ? 1 / parameters.uptime_hours : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel(() => {\n return { Plant: [{ up: 0 }] };\n});", + "x": 1755, + "y": 2760 + }, + { + "id": "transition__plant_recovers", + "name": "Restart the air separation plant", + "inputArcs": [ + { + "placeId": "place__plant", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__plant", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.Plant[0].up === 0 ? 1 / parameters.repair_hours : 1e-9;\n});", + "transitionKernelCode": "export default TransitionKernel(() => {\n return { Plant: [{ up: 1 }] };\n});", + "x": 1755, + "y": 2970 + } + ], + "types": [ + { + "id": "type__tanker", + "name": "Tanker", + "iconSlug": "circle", + "displayColor": "#0ea5e9", + "elements": [ + { + "elementId": "type__tanker__remaining", + "name": "remaining", + "type": "real" + }, + { + "elementId": "type__tanker__product", + "name": "product", + "type": "string" + }, + { + "elementId": "type__tanker__payload", + "name": "payload", + "type": "real" + }, + { + "elementId": "type__tanker__hired", + "name": "hired", + "type": "integer" + } + ] + }, + { + "id": "type__tank", + "name": "Tank", + "iconSlug": "circle", + "displayColor": "#f97316", + "elements": [ + { + "elementId": "type__tank__level", + "name": "level", + "type": "real" + }, + { + "elementId": "type__tank__pressure", + "name": "pressure", + "type": "real" + }, + { + "elementId": "type__tank__capacity", + "name": "capacity", + "type": "real" + }, + { + "elementId": "type__tank__draw", + "name": "draw", + "type": "real" + }, + { + "elementId": "type__tank__drawn", + "name": "drawn", + "type": "real" + }, + { + "elementId": "type__tank__boiled", + "name": "boiled", + "type": "real" + }, + { + "elementId": "type__tank__criticality", + "name": "criticality", + "type": "integer" + }, + { + "elementId": "type__tank__product", + "name": "product", + "type": "string" + }, + { + "elementId": "type__tank__spilled", + "name": "spilled", + "type": "real" + } + ] + }, + { + "id": "type__plant", + "name": "Plant", + "iconSlug": "circle", + "displayColor": "#ef4444", + "elements": [ + { + "elementId": "type__plant__up", + "name": "up", + "type": "integer" + } + ] + } + ], + "differentialEquations": [ + { + "id": "de__tank", + "name": "Tank", + "colorId": "type__tank", + "code": "// The rung the whole domain is built for. Level falls from the customer's draw\n// and from boil-off together, and stops at empty so Euler cannot drive it\n// negative. Pressure rises as boil-off gas fills whatever ullage is left, so a\n// nearly full tank pressurises fastest, and falls as liquid is drawn off. Empty\n// stops the customer's line and full opens the relief valve, so the safe region\n// is an interval and \"hold more stock\" is not a safe default.\nexport default Dynamics((tokens, parameters) => {\n return tokens.map((unit) => ({\n level: unit.level > 0 ? -(unit.draw + parameters.boiloff_rate) : 0, pressure: Math.max(parameters.pressure_gain * parameters.boiloff_rate / Math.max(unit.capacity - unit.level, 1) - parameters.pressure_vented_by_draw * unit.draw, unit.pressure > 1 ? -1 : 0), capacity: 0, draw: 0, drawn: unit.level > 0 ? unit.draw : 0, boiled: unit.level > 0 ? parameters.boiloff_rate : 0, spilled: 0\n }));\n});" + }, + { + "id": "de__on_route", + "name": "Journey clock (on route)", + "colorId": "type__tanker", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({ remaining: -1, payload: 0 }));\n});" + }, + { + "id": "de__returning", + "name": "Journey clock (returning)", + "colorId": "type__tanker", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({ remaining: -1, payload: 0 }));\n});" + } + ], + "parameters": [ + { + "id": "param__boiloff_rate", + "name": "Boil-off rate", + "variableName": "boiloff_rate", + "type": "real", + "defaultValue": "0.16" + }, + { + "id": "param__draw_1", + "name": "SteadyNitrogen draw rate", + "variableName": "draw_1", + "type": "real", + "defaultValue": "0.8" + }, + { + "id": "param__draw_2", + "name": "SlowNitrogen draw rate", + "variableName": "draw_2", + "type": "real", + "defaultValue": "0.1" + }, + { + "id": "param__draw_3", + "name": "CriticalOxygen draw rate", + "variableName": "draw_3", + "type": "real", + "defaultValue": "0.6" + }, + { + "id": "param__route_scale", + "name": "Route scale", + "variableName": "route_scale", + "type": "real", + "defaultValue": "1" + }, + { + "id": "param__route_spread", + "name": "Route spread", + "variableName": "route_spread", + "type": "real", + "defaultValue": "0.35" + }, + { + "id": "param__return_time", + "name": "Return leg (hours)", + "variableName": "return_time", + "type": "real", + "defaultValue": "4.0" + }, + { + "id": "param__trigger_1", + "name": "SteadyNitrogen trigger level", + "variableName": "trigger_1", + "type": "real", + "defaultValue": "16" + }, + { + "id": "param__trigger_2", + "name": "SlowNitrogen trigger level", + "variableName": "trigger_2", + "type": "real", + "defaultValue": "6" + }, + { + "id": "param__trigger_3", + "name": "CriticalOxygen trigger level", + "variableName": "trigger_3", + "type": "real", + "defaultValue": "20" + }, + { + "id": "param__pressure_gain", + "name": "Pressure gain", + "variableName": "pressure_gain", + "type": "real", + "defaultValue": "20" + }, + { + "id": "param__pressure_vented_by_draw", + "name": "Pressure vented by draw", + "variableName": "pressure_vented_by_draw", + "type": "real", + "defaultValue": "3" + }, + { + "id": "param__relief_setpoint", + "name": "Relief setpoint", + "variableName": "relief_setpoint", + "type": "real", + "defaultValue": "8" + }, + { + "id": "param__relief_reseat", + "name": "Relief reseat margin", + "variableName": "relief_reseat", + "type": "real", + "defaultValue": "1" + }, + { + "id": "param__vent_loss", + "name": "Units lost per valve opening", + "variableName": "vent_loss", + "type": "real", + "defaultValue": "0.4" + }, + { + "id": "param__hire_enabled", + "name": "Hire enabled", + "variableName": "hire_enabled", + "type": "real", + "defaultValue": "1" + }, + { + "id": "param__hire_oxygen", + "name": "Hire oxygen trailers (0 nitrogen, 1 oxygen)", + "variableName": "hire_oxygen", + "type": "integer", + "defaultValue": "0" + }, + { + "id": "param__hired_payload", + "name": "Hired payload", + "variableName": "hired_payload", + "type": "real", + "defaultValue": "12" + }, + { + "id": "param__uptime_hours", + "name": "Mean hours between trips", + "variableName": "uptime_hours", + "type": "real", + "defaultValue": "90" + }, + { + "id": "param__repair_hours", + "name": "Mean hours to recover", + "variableName": "repair_hours", + "type": "real", + "defaultValue": "24" + }, + { + "id": "param__outage_route_penalty", + "name": "Re-sourced route penalty", + "variableName": "outage_route_penalty", + "type": "real", + "defaultValue": "2.2" + }, + { + "id": "param__loading_rate", + "name": "Depot loading rate", + "variableName": "loading_rate", + "type": "real", + "defaultValue": "4" + }, + { + "id": "param__hire_rate", + "name": "Spot hire arrival rate", + "variableName": "hire_rate", + "type": "real", + "defaultValue": "0.1" + } + ], + "scenarios": [ + { + "id": "scenario__base", + "name": "Three tankers, normal routes", + "description": "The reference case: three tankers on the depot, routes at their nominal length.", + "scenarioParameters": [], + "parameterOverrides": {}, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"oxygen\", payload: 12, hired: 0 }],\n LoadsDelivered: 0,\n Returning: [],\n OpenOrders: 0,\n Hires: 0,\n Plant: [{ up: 1 }],\n Outages: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenTank: [{ level: 42, pressure: 2, drawn: 0, boiled: 0, capacity: 54, draw: parameters.draw_1, criticality: 2, product: \"nitrogen\", spilled: 0 }],\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenTank: [{ level: 18, pressure: 2, drawn: 0, boiled: 0, capacity: 30, draw: parameters.draw_2, criticality: 1, product: \"nitrogen\", spilled: 0 }],\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenTank: [{ level: 46, pressure: 2, drawn: 0, boiled: 0, capacity: 58, draw: parameters.draw_3, criticality: 3, product: \"oxygen\", spilled: 0 }],\n};" + } + }, + { + "id": "scenario__slow_routes", + "name": "Three tankers, routes half again as long", + "description": "Winter roads. Every route stretches by half, which is a question about the tail of the delay rather than its mean.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "route_scale", + "default": 1.5 + } + ], + "parameterOverrides": { + "param__route_scale": "scenario.route_scale" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"oxygen\", payload: 12, hired: 0 }],\n LoadsDelivered: 0,\n Returning: [],\n OpenOrders: 0,\n Hires: 0,\n Plant: [{ up: 1 }],\n Outages: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenTank: [{ level: 42, pressure: 2, drawn: 0, boiled: 0, capacity: 54, draw: parameters.draw_1, criticality: 2, product: \"nitrogen\", spilled: 0 }],\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenTank: [{ level: 18, pressure: 2, drawn: 0, boiled: 0, capacity: 30, draw: parameters.draw_2, criticality: 1, product: \"nitrogen\", spilled: 0 }],\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenTank: [{ level: 46, pressure: 2, drawn: 0, boiled: 0, capacity: 58, draw: parameters.draw_3, criticality: 3, product: \"oxygen\", spilled: 0 }],\n};" + } + }, + { + "id": "scenario__two_tankers", + "name": "Two tankers, normal routes", + "description": "One trailer off the road. What the fleet can absorb.", + "scenarioParameters": [], + "parameterOverrides": {}, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"oxygen\", payload: 12, hired: 0 }],\n LoadsDelivered: 0,\n Returning: [],\n OpenOrders: 0,\n Hires: 0,\n Plant: [{ up: 1 }],\n Outages: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenTank: [{ level: 42, pressure: 2, drawn: 0, boiled: 0, capacity: 54, draw: parameters.draw_1, criticality: 2, product: \"nitrogen\", spilled: 0 }],\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenTank: [{ level: 18, pressure: 2, drawn: 0, boiled: 0, capacity: 30, draw: parameters.draw_2, criticality: 1, product: \"nitrogen\", spilled: 0 }],\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenTank: [{ level: 46, pressure: 2, drawn: 0, boiled: 0, capacity: 58, draw: parameters.draw_3, criticality: 3, product: \"oxygen\", spilled: 0 }],\n};" + } + }, + { + "id": "scenario__slow_customer_throttled", + "name": "SlowNitrogen throttled back", + "description": "The slow-drawing customer cuts to a fifth of its usual draw. Its tank now sits nearly full with boil-off gas filling a small ullage, so the relief valve starts to cycle. This is the case where filling a tank up is the wrong thing to do.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "slow_draw", + "default": 0.01 + } + ], + "parameterOverrides": { + "param__draw_2": "scenario.slow_draw" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"oxygen\", payload: 12, hired: 0 }],\n LoadsDelivered: 0,\n Returning: [],\n OpenOrders: 0,\n Hires: 0,\n Plant: [{ up: 1 }],\n Outages: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenTank: [{ level: 42, pressure: 2, drawn: 0, boiled: 0, capacity: 54, draw: parameters.draw_1, criticality: 2, product: \"nitrogen\", spilled: 0 }],\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenTank: [{ level: 18, pressure: 2, drawn: 0, boiled: 0, capacity: 30, draw: parameters.draw_2, criticality: 1, product: \"nitrogen\", spilled: 0 }],\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenTank: [{ level: 46, pressure: 2, drawn: 0, boiled: 0, capacity: 58, draw: parameters.draw_3, criticality: 3, product: \"oxygen\", spilled: 0 }],\n};" + } + }, + { + "id": "scenario__no_hire", + "name": "No spot hire", + "description": "The same net with hiring switched off, so the fleet is fixed at three. The difference against the base case is what the ability to hire is worth.", + "scenarioParameters": [ + { + "type": "real", + "identifier": "hire_enabled", + "default": 0 + } + ], + "parameterOverrides": { + "param__hire_enabled": "scenario.hire_enabled" + }, + "initialState": { + "type": "code", + "content": "return {\n IdleTankers: [{ remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"nitrogen\", payload: 12, hired: 0 }, { remaining: 0, product: \"oxygen\", payload: 12, hired: 0 }],\n LoadsDelivered: 0,\n Returning: [],\n OpenOrders: 0,\n Hires: 0,\n Plant: [{ up: 1 }],\n Outages: 0,\n SteadyNitrogenOrderPlaced: 0,\n SteadyNitrogenOrderPermits: 2,\n SteadyNitrogenOnRoute: [],\n SteadyNitrogenVented: 0,\n SteadyNitrogenLineRunning: 1,\n SteadyNitrogenLineStopped: 0,\n SteadyNitrogenStockouts: 0,\n SteadyNitrogenTank: [{ level: 42, pressure: 2, drawn: 0, boiled: 0, capacity: 54, draw: parameters.draw_1, criticality: 2, product: \"nitrogen\", spilled: 0 }],\n SlowNitrogenOrderPlaced: 0,\n SlowNitrogenOrderPermits: 1,\n SlowNitrogenOnRoute: [],\n SlowNitrogenVented: 0,\n SlowNitrogenLineRunning: 1,\n SlowNitrogenLineStopped: 0,\n SlowNitrogenStockouts: 0,\n SlowNitrogenTank: [{ level: 18, pressure: 2, drawn: 0, boiled: 0, capacity: 30, draw: parameters.draw_2, criticality: 1, product: \"nitrogen\", spilled: 0 }],\n CriticalOxygenOrderPlaced: 0,\n CriticalOxygenOrderPermits: 2,\n CriticalOxygenOnRoute: [],\n CriticalOxygenVented: 0,\n CriticalOxygenLineRunning: 1,\n CriticalOxygenLineStopped: 0,\n CriticalOxygenStockouts: 0,\n CriticalOxygenTank: [{ level: 46, pressure: 2, drawn: 0, boiled: 0, capacity: 58, draw: parameters.draw_3, criticality: 3, product: \"oxygen\", spilled: 0 }],\n};" + } + } + ], + "metrics": [ + { + "id": "metric__deliveries", + "name": "Loads delivered", + "description": "Tanker drops made.", + "code": "return state.places.LoadsDelivered.count;" + }, + { + "id": "metric__stockouts", + "name": "Stockouts", + "description": "Times a customer line stopped for want of product.", + "code": "return state.places.SteadyNitrogenStockouts.count + state.places.SlowNitrogenStockouts.count + state.places.CriticalOxygenStockouts.count;" + }, + { + "id": "metric__weighted_stockouts", + "name": "Criticality-weighted stockouts", + "description": "Stockouts weighted by how much the customer matters: the metals plant counts 3, the freezing plant 2, the laser shop 1. A total count cannot say whether the outages landed on the customer you could least afford to lose.", + "code": "return 2 * state.places.SteadyNitrogenStockouts.count + 1 * state.places.SlowNitrogenStockouts.count + 3 * state.places.CriticalOxygenStockouts.count;" + }, + { + "id": "metric__vented", + "name": "Vented through relief", + "description": "Units lost through a relief valve, in units at every level. Below the continuous levels the Vented place holds one token per unit, because the tank is a pile of one-unit tokens. From the continuous levels it holds one token per valve opening, each worth vent_loss units, so the count is scaled to keep this metric comparable down the sequence. Structurally reachable everywhere, and unreachable in practice below the continuous levels: under a level trigger the tank never fills completely, and with no pressure nothing else opens the valve.", + "code": "return parameters.vent_loss * (state.places.SteadyNitrogenVented.count + state.places.SlowNitrogenVented.count + state.places.CriticalOxygenVented.count);" + }, + { + "id": "metric__evaporated", + "name": "Evaporated", + "description": "Units lost to boil-off.", + "code": "const boiled1 = state.places.SteadyNitrogenTank.tokens.reduce((sum, tank) => sum + tank.boiled, 0);\nconst boiled2 = state.places.SlowNitrogenTank.tokens.reduce((sum, tank) => sum + tank.boiled, 0);\nconst boiled3 = state.places.CriticalOxygenTank.tokens.reduce((sum, tank) => sum + tank.boiled, 0);\nreturn boiled1 + boiled2 + boiled3;" + }, + { + "id": "metric__spilled", + "name": "Surplus lost on delivery", + "description": "Units a tanker could not fit into the tank. From this level the arrival kernel fills the tank to capacity and drops the remainder, where levels 1 to 3 held the delivery back until it fit. It reads 0 in every scenario here, because the reorder trigger plus the largest possible outstanding order stays at least 14 units below capacity in all three tanks (40 of 54, 18 of 30, 44 of 58), so a load always fits. The metric guards that headroom against a change in trigger, payload or permit count.", + "code": "const spilled1 = state.places.SteadyNitrogenTank.tokens.reduce((sum, tank) => sum + tank.spilled, 0);\nconst spilled2 = state.places.SlowNitrogenTank.tokens.reduce((sum, tank) => sum + tank.spilled, 0);\nconst spilled3 = state.places.CriticalOxygenTank.tokens.reduce((sum, tank) => sum + tank.spilled, 0);\nreturn spilled1 + spilled2 + spilled3;" + }, + { + "id": "metric__consumed", + "name": "Consumed", + "description": "Units the customers actually used.", + "code": "const drawn1 = state.places.SteadyNitrogenTank.tokens.reduce((sum, tank) => sum + tank.drawn, 0);\nconst drawn2 = state.places.SlowNitrogenTank.tokens.reduce((sum, tank) => sum + tank.drawn, 0);\nconst drawn3 = state.places.CriticalOxygenTank.tokens.reduce((sum, tank) => sum + tank.drawn, 0);\nreturn drawn1 + drawn2 + drawn3;" + }, + { + "id": "metric__stockouts_per_hundred", + "name": "Stockouts per 100 units delivered to customers", + "description": "Stockouts against the volume actually drawn. A demand process that cannot go negative consumes more when it is more volatile, so a raw count would credit a volatile scenario for being busier as well as worse.", + "code": "const drawn1 = state.places.SteadyNitrogenTank.tokens.reduce((sum, tank) => sum + tank.drawn, 0);\nconst drawn2 = state.places.SlowNitrogenTank.tokens.reduce((sum, tank) => sum + tank.drawn, 0);\nconst drawn3 = state.places.CriticalOxygenTank.tokens.reduce((sum, tank) => sum + tank.drawn, 0);\nconst drawnTotal = drawn1 + drawn2 + drawn3;\nreturn drawnTotal > 0 ? 100 * (state.places.SteadyNitrogenStockouts.count + state.places.SlowNitrogenStockouts.count + state.places.CriticalOxygenStockouts.count) / drawnTotal : 0;" + }, + { + "id": "metric__stockouts_1", + "name": "SteadyNitrogen stockouts", + "description": "Times the food freezing plant stopped.", + "code": "return state.places.SteadyNitrogenStockouts.count;" + }, + { + "id": "metric__stockouts_2", + "name": "SlowNitrogen stockouts", + "description": "Times the laser cutting shop stopped.", + "code": "return state.places.SlowNitrogenStockouts.count;" + }, + { + "id": "metric__stockouts_3", + "name": "CriticalOxygen stockouts", + "description": "Times the metals plant stopped.", + "code": "return state.places.CriticalOxygenStockouts.count;" + }, + { + "id": "metric__level", + "name": "Level in tanks", + "description": "Units left across the three customer tanks at the end.", + "code": "const level1 = state.places.SteadyNitrogenTank.tokens.reduce((sum, tank) => sum + tank.level, 0);\nconst level2 = state.places.SlowNitrogenTank.tokens.reduce((sum, tank) => sum + tank.level, 0);\nconst level3 = state.places.CriticalOxygenTank.tokens.reduce((sum, tank) => sum + tank.level, 0);\nreturn level1 + level2 + level3;" + }, + { + "id": "metric__vent_openings", + "name": "Relief valve openings", + "description": "Times a relief valve lifted. The valve reseats below the setpoint, so a tank that stays near it cycles, and the count of openings says how hard the valve is working where the units vented say what it cost.", + "code": "return state.places.SteadyNitrogenVented.count + state.places.SlowNitrogenVented.count + state.places.CriticalOxygenVented.count;" + }, + { + "id": "metric__pressure", + "name": "Pressure at SlowNitrogen", + "description": "Tank pressure at the slow-drawing customer, where boil-off gas has the least ullage to fill.", + "code": "const tanks = state.places.SlowNitrogenTank.tokens;\nif (tanks.length === 0) return 0;\nreturn tanks[0].pressure;" + }, + { + "id": "metric__hires", + "name": "Tankers hired in", + "description": "Trailers created on demand. A fixed-population net cannot represent this at all.", + "code": "return state.places.Hires.count;" + }, + { + "id": "metric__outages", + "name": "Plant outages", + "description": "Times the air separation plant tripped and its customers were re-sourced onto longer routes.", + "code": "return state.places.Outages.count;" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Gases 4 \u2014 dynamic coloured net" +} diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/semiconductor-fab-drift-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/semiconductor-fab-drift-layout.json new file mode 100644 index 00000000000..5696182b13f --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/semiconductor-fab-drift-layout.json @@ -0,0 +1,1509 @@ +{ + "places": [ + { + "id": "place__fab_entrance", + "name": "FabEntrance", + "colorId": "type__lot", + "dynamicsEnabled": true, + "differentialEquationId": "de__aging", + "showAsInitialState": false, + "x": 2360, + "y": 423.5 + }, + { + "id": "place__wip_queue", + "name": "WIPQueue", + "colorId": "type__lot", + "dynamicsEnabled": true, + "differentialEquationId": "de__aging", + "showAsInitialState": false, + "x": -255, + "y": 1515 + }, + { + "id": "place__batch_queue", + "name": "BatchQueue", + "colorId": "type__lot", + "dynamicsEnabled": true, + "differentialEquationId": "de__batch_wait", + "showAsInitialState": false, + "x": 375, + "y": 1515 + }, + { + "id": "place__in_process", + "name": "InProcess", + "colorId": "type__lot", + "dynamicsEnabled": true, + "differentialEquationId": "de__processing", + "showAsInitialState": false, + "x": 975, + "y": 1515 + }, + { + "id": "place__post_process", + "name": "PostProcess", + "colorId": "type__lot", + "dynamicsEnabled": true, + "differentialEquationId": "de__aging", + "showAsInitialState": false, + "x": 1800, + "y": 569.3333333333334 + }, + { + "id": "place__in_inspection", + "name": "InInspection", + "colorId": "type__lot", + "dynamicsEnabled": true, + "differentialEquationId": "de__processing", + "showAsInitialState": false, + "x": 930, + "y": 585 + }, + { + "id": "place__finished", + "name": "Finished", + "colorId": "type__lot", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2360, + "y": 256 + }, + { + "id": "place__scrapped", + "name": "Scrapped", + "colorId": "type__lot", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2355, + "y": 885 + }, + { + "id": "place__wip_permits", + "name": "WIPPermits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2355, + "y": 1110 + }, + { + "id": "place__chambers_available", + "name": "ChambersAvailable", + "colorId": "type__chamber", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 0, + "y": 1020 + }, + { + "id": "place__chambers_processing", + "name": "ChambersProcessing", + "colorId": "type__chamber", + "dynamicsEnabled": true, + "differentialEquationId": "de__chamber_processing", + "showAsInitialState": false, + "x": 930, + "y": 1020 + }, + { + "id": "place__chambers_in_maintenance", + "name": "ChambersInMaintenance", + "colorId": "type__chamber", + "dynamicsEnabled": true, + "differentialEquationId": "de__chamber_maintenance", + "showAsInitialState": false, + "x": 1875, + "y": 1515 + }, + { + "id": "place__chambers_broken", + "name": "ChambersBroken", + "colorId": "type__chamber", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1800, + "y": 930 + }, + { + "id": "place__maintenance_crew", + "name": "MaintenanceCrew", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 1260, + "y": 1230 + }, + { + "id": "place__maintenance_events", + "name": "MaintenanceEvents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1800, + "y": 1080 + }, + { + "id": "place__breakdown_events", + "name": "BreakdownEvents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1800, + "y": 1231 + }, + { + "id": "place__lots_released", + "name": "LotsReleased", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2925, + "y": 1110 + }, + { + "id": "place__lots_completed", + "name": "LotsCompleted", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2360, + "y": 106 + }, + { + "id": "place__calibrations", + "name": "Calibrations", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2505, + "y": 1515 + } + ], + "transitions": [ + { + "id": "transition__demand_logic", + "name": "Demand arrives (logic)", + "inputArcs": [], + "outputArcs": [ + { + "placeId": "place__fab_entrance", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.demand_rate * 0.5;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n FabEntrance: [{ product_type: 0, layer: 0, priority: 1.0, age: 0, defect_count: 0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0, batch_wait: 0 }],\n };\n});", + "x": 2080, + "y": 456 + }, + { + "id": "transition__demand_memory", + "name": "Demand arrives (memory)", + "inputArcs": [], + "outputArcs": [ + { + "placeId": "place__fab_entrance", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.demand_rate * 0.35;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n FabEntrance: [{ product_type: 1, layer: 0, priority: 1.0, age: 0, defect_count: 0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0, batch_wait: 0 }],\n };\n});", + "x": 2080, + "y": 156 + }, + { + "id": "transition__demand_analog", + "name": "Demand arrives (analog)", + "inputArcs": [], + "outputArcs": [ + { + "placeId": "place__fab_entrance", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.demand_rate * 0.15;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n FabEntrance: [{ product_type: 2, layer: 0, priority: 1.0, age: 0, defect_count: 0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0, batch_wait: 0 }],\n };\n});", + "x": 2080, + "y": 356 + }, + { + "id": "transition__release_lot", + "name": "Release lot into fab", + "inputArcs": [ + { + "placeId": "place__fab_entrance", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__wip_permits", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1 + }, + { + "placeId": "place__lots_released", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => true);", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const lot = input.FabEntrance[0];\n return {\n WIPQueue: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": 2595, + "y": 1110 + }, + { + "id": "transition__priority_update", + "name": "Update lot priority toward deadline", + "inputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.WIPQueue[0];\n return lot.wait_time >= parameters.priority_update_interval;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.WIPQueue[0];\n const remaining = lot.due_date - lot.age;\n const urgency = remaining <= 0 ? 10 : parameters.target_cycle_time / remaining;\n const newPriority = Math.max(urgency, 1.0);\n return {\n WIPQueue: [{ product_type: lot.product_type, layer: lot.layer, priority: newPriority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": -255, + "y": 1110 + }, + { + "id": "transition__extend_deadline", + "name": "Renegotiate deadline (lot past due)", + "inputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.WIPQueue[0];\n return lot.age > lot.due_date + parameters.deadline_grace_period;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.WIPQueue[0];\n return {\n WIPQueue: [{ product_type: lot.product_type, layer: lot.layer, priority: 1.0, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: 0, due_date: lot.age + parameters.target_cycle_time, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": -255, + "y": 1290 + }, + { + "id": "transition__dispatch_litho", + "name": "Dispatch lot to litho chamber", + "inputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_process", + "weight": 1 + }, + { + "placeId": "place__chambers_processing", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.WIPQueue[0];\n const chamber = input.ChambersAvailable[0];\n const qualOk = chamber.qualification === 0\n || (chamber.qualification === 1 && lot.product_type <= 1)\n || (chamber.qualification === 2 && lot.product_type === 2);\n return chamber.machine_group === 0 && qualOk && (lot.layer === 0 || lot.layer === 4 || lot.layer === 9 || lot.layer === 12 || lot.layer === 16 || lot.layer === 20 || lot.layer === 24);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.WIPQueue[0];\n const chamber = input.ChambersAvailable[0];\n const productFactor = lot.product_type === 0 ? 1.0\n : lot.product_type === 1 ? 0.85 : 1.15;\n return {\n InProcess: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: Distribution.Lognormal(Math.log(parameters.litho_time * productFactor), parameters.process_sigma), wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: 0 }],\n ChambersProcessing: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: parameters.diffusion_step, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 465, + "y": 1125 + }, + { + "id": "transition__dispatch_etch", + "name": "Dispatch lot to etch chamber", + "inputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_process", + "weight": 1 + }, + { + "placeId": "place__chambers_processing", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.WIPQueue[0];\n const chamber = input.ChambersAvailable[0];\n const qualOk = chamber.qualification === 0\n || (chamber.qualification === 1 && lot.product_type <= 1)\n || (chamber.qualification === 2 && lot.product_type === 2);\n return chamber.machine_group === 1 && qualOk && (lot.layer === 1 || lot.layer === 5 || lot.layer === 8 || lot.layer === 10 || lot.layer === 13 || lot.layer === 15 || lot.layer === 17 || lot.layer === 21 || lot.layer === 25 || lot.layer === 27);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.WIPQueue[0];\n const chamber = input.ChambersAvailable[0];\n const productFactor = lot.product_type === 0 ? 1.0\n : lot.product_type === 1 ? 0.85 : 1.15;\n return {\n InProcess: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: Distribution.Lognormal(Math.log(parameters.etch_time * productFactor), parameters.process_sigma), wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: 0 }],\n ChambersProcessing: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: parameters.diffusion_step, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 465, + "y": 915 + }, + { + "id": "transition__enter_batch_queue", + "name": "Lot enters furnace batch queue", + "inputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__batch_queue", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const lot = input.WIPQueue[0];\n return lot.layer === 2 || lot.layer === 7 || lot.layer === 14 || lot.layer === 19 || lot.layer === 22 || lot.layer === 26;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const lot = input.WIPQueue[0];\n return {\n BatchQueue: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: 0 }],\n };\n});", + "x": 105, + "y": 1515 + }, + { + "id": "transition__load_furnace", + "name": "Load lot into furnace batch", + "inputArcs": [ + { + "placeId": "place__batch_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_process", + "weight": 1 + }, + { + "placeId": "place__chambers_available", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.BatchQueue[0];\n const chamber = input.ChambersAvailable[0];\n const qualOk = chamber.qualification === 0\n || (chamber.qualification === 1 && lot.product_type <= 1)\n || (chamber.qualification === 2 && lot.product_type === 2);\n return chamber.machine_group === 2 && qualOk\n && chamber.batch_count < parameters.batch_size;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.BatchQueue[0];\n const chamber = input.ChambersAvailable[0];\n const productFactor = lot.product_type === 0 ? 1.0\n : lot.product_type === 1 ? 0.85 : 1.15;\n return {\n InProcess: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: Distribution.Lognormal(Math.log(parameters.furnace_time * productFactor), parameters.process_sigma), wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: 0 }],\n ChambersAvailable: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: chamber.diffusion_clock, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count + 1 }],\n };\n});", + "x": 660, + "y": 1515 + }, + { + "id": "transition__start_furnace_full", + "name": "Start furnace (batch full)", + "inputArcs": [ + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_processing", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const chamber = input.ChambersAvailable[0];\n return chamber.machine_group === 2\n && chamber.batch_count >= parameters.batch_size;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersAvailable[0];\n return {\n ChambersProcessing: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: parameters.diffusion_step, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 465, + "y": 1020 + }, + { + "id": "transition__start_furnace_timeout", + "name": "Start furnace (batch timeout)", + "inputArcs": [ + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__batch_queue", + "weight": 1, + "type": "read" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_processing", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const chamber = input.ChambersAvailable[0];\n const lot = input.BatchQueue[0];\n return chamber.machine_group === 2\n && chamber.batch_count > 0\n && chamber.batch_count < parameters.batch_size\n && lot.batch_wait >= parameters.batch_timeout;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersAvailable[0];\n return {\n ChambersProcessing: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: parameters.diffusion_step, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 465, + "y": 1215 + }, + { + "id": "transition__dispatch_inspect", + "name": "Dispatch lot to inspection", + "inputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_inspection", + "weight": 1 + }, + { + "placeId": "place__chambers_processing", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.WIPQueue[0];\n const chamber = input.ChambersAvailable[0];\n return chamber.machine_group === 3 && (lot.layer === 3 || lot.layer === 6 || lot.layer === 11 || lot.layer === 18 || lot.layer === 23);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.WIPQueue[0];\n const chamber = input.ChambersAvailable[0];\n return {\n InInspection: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: Distribution.Lognormal(Math.log(parameters.inspect_time), parameters.process_sigma), wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n ChambersProcessing: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: parameters.diffusion_step, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 465, + "y": 810 + }, + { + "id": "transition__process_complete_ok", + "name": "Processing complete, chamber ok", + "inputArcs": [ + { + "placeId": "place__in_process", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_processing", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__post_process", + "weight": 1 + }, + { + "placeId": "place__chambers_available", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.InProcess[0];\n const chamber = input.ChambersProcessing[0];\n return lot.process_remaining <= 0 && chamber.condition < parameters.maintenance_threshold;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.InProcess[0];\n const chamber = input.ChambersProcessing[0];\n const defectRate = parameters.base_defect_rate\n * (1 + parameters.condition_sensitivity * chamber.condition)\n * (1 + parameters.particle_sensitivity * chamber.particle_count)\n * (1 + parameters.drift_defect_factor * Math.abs(chamber.process_drift));\n return {\n PostProcess: [{ product_type: lot.product_type, layer: lot.layer + 1, priority: lot.priority, age: lot.age, defect_count: Distribution.Lognormal(Math.log(Math.max(lot.defect_count + defectRate, 0.001)), 0.5), process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias + chamber.process_drift, batch_wait: lot.batch_wait }],\n ChambersAvailable: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: chamber.diffusion_clock, lots_processed: chamber.lots_processed + 1, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: 0 }],\n };\n});", + "x": 1530, + "y": 690 + }, + { + "id": "transition__process_complete_maintenance", + "name": "Processing complete, chamber needs maintenance", + "inputArcs": [ + { + "placeId": "place__in_process", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_processing", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__post_process", + "weight": 1 + }, + { + "placeId": "place__chambers_in_maintenance", + "weight": 1 + }, + { + "placeId": "place__maintenance_events", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.InProcess[0];\n const chamber = input.ChambersProcessing[0];\n return lot.process_remaining <= 0 && chamber.condition >= parameters.maintenance_threshold;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const lot = input.InProcess[0];\n const chamber = input.ChambersProcessing[0];\n const defectRate = parameters.base_defect_rate\n * (1 + parameters.condition_sensitivity * chamber.condition)\n * (1 + parameters.particle_sensitivity * chamber.particle_count)\n * (1 + parameters.drift_defect_factor * Math.abs(chamber.process_drift));\n return {\n PostProcess: [{ product_type: lot.product_type, layer: lot.layer + 1, priority: lot.priority, age: lot.age, defect_count: Distribution.Lognormal(Math.log(Math.max(lot.defect_count + defectRate, 0.001)), 0.5), process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias + chamber.process_drift, batch_wait: lot.batch_wait }],\n ChambersInMaintenance: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: Distribution.Lognormal(Math.log(parameters.maintenance_duration), parameters.maintenance_sigma), diffusion_clock: 0, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 1530, + "y": 915 + }, + { + "id": "transition__route_to_queue", + "name": "Route lot back to WIP queue", + "inputArcs": [ + { + "placeId": "place__post_process", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const lot = input.PostProcess[0];\n return lot.layer < 28;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const lot = input.PostProcess[0];\n return {\n WIPQueue: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": 2085, + "y": 570 + }, + { + "id": "transition__lot_passes", + "name": "Lot passes final test", + "inputArcs": [ + { + "placeId": "place__post_process", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__finished", + "weight": 1 + }, + { + "placeId": "place__wip_permits", + "weight": 1 + }, + { + "placeId": "place__lots_completed", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.PostProcess[0];\n return lot.layer >= 28 && lot.defect_count < parameters.scrap_threshold;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const lot = input.PostProcess[0];\n return {\n Finished: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": 2080, + "y": 256 + }, + { + "id": "transition__lot_fails", + "name": "Lot fails final test", + "inputArcs": [ + { + "placeId": "place__post_process", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__scrapped", + "weight": 1 + }, + { + "placeId": "place__wip_permits", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const lot = input.PostProcess[0];\n return lot.layer >= 28 && lot.defect_count >= parameters.scrap_threshold;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const lot = input.PostProcess[0];\n return {\n Scrapped: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": 2080, + "y": 772 + }, + { + "id": "transition__inspection_complete", + "name": "Inspection complete", + "inputArcs": [ + { + "placeId": "place__in_inspection", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__chambers_processing", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__wip_queue", + "weight": 1 + }, + { + "placeId": "place__chambers_available", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const lot = input.InInspection[0];\n return lot.process_remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const lot = input.InInspection[0];\n const chamber = input.ChambersProcessing[0];\n return {\n WIPQueue: [{ product_type: lot.product_type, layer: lot.layer + 1, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: 0, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n ChambersAvailable: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: chamber.diffusion_clock, lots_processed: chamber.lots_processed + 1, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: 0 }],\n };\n});", + "x": 1185, + "y": 585 + }, + { + "id": "transition__start_maintenance", + "name": "Start preventive maintenance", + "inputArcs": [ + { + "placeId": "place__chambers_available", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__maintenance_crew", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_in_maintenance", + "weight": 1 + }, + { + "placeId": "place__maintenance_events", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const chamber = input.ChambersAvailable[0];\n return chamber.condition >= parameters.maintenance_threshold;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersAvailable[0];\n return {\n ChambersInMaintenance: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: Distribution.Lognormal(Math.log(parameters.maintenance_duration), parameters.maintenance_sigma), diffusion_clock: 0, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 1515, + "y": 1395 + }, + { + "id": "transition__maintenance_complete", + "name": "Maintenance complete (drift recalibrated)", + "inputArcs": [ + { + "placeId": "place__chambers_in_maintenance", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_available", + "weight": 1 + }, + { + "placeId": "place__maintenance_crew", + "weight": 1 + }, + { + "placeId": "place__calibrations", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.ChambersInMaintenance[0].maintenance_remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersInMaintenance[0];\n return {\n ChambersAvailable: [{ condition: 0, particle_count: 0.05, hours_since_maintenance: 0, maintenance_remaining: 0, diffusion_clock: chamber.diffusion_clock, lots_processed: 0, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: Distribution.Gaussian(0, parameters.calibration_residual), batch_count: 0 }],\n };\n});", + "x": 2190, + "y": 1515 + }, + { + "id": "transition__breakdown", + "name": "Chamber breakdown", + "inputArcs": [ + { + "placeId": "place__chambers_processing", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__in_process", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__maintenance_crew", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_in_maintenance", + "weight": 1 + }, + { + "placeId": "place__scrapped", + "weight": 1 + }, + { + "placeId": "place__breakdown_events", + "weight": 1 + }, + { + "placeId": "place__wip_permits", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n const chamber = input.ChambersProcessing[0];\n return parameters.breakdown_base_rate\n * Math.exp(parameters.breakdown_condition_factor * chamber.condition);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersProcessing[0];\n const lot = input.InProcess[0];\n return {\n ChambersInMaintenance: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: Distribution.Lognormal(Math.log(parameters.breakdown_repair_time), parameters.breakdown_sigma), diffusion_clock: 0, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n Scrapped: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": 1305, + "y": 1515 + }, + { + "id": "transition__breakdown_no_crew", + "name": "Chamber breakdown (no crew available)", + "inputArcs": [ + { + "placeId": "place__chambers_processing", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__in_process", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__maintenance_crew", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_broken", + "weight": 1 + }, + { + "placeId": "place__scrapped", + "weight": 1 + }, + { + "placeId": "place__breakdown_events", + "weight": 1 + }, + { + "placeId": "place__wip_permits", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n const chamber = input.ChambersProcessing[0];\n return parameters.breakdown_base_rate\n * Math.exp(parameters.breakdown_condition_factor * chamber.condition);\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersProcessing[0];\n const lot = input.InProcess[0];\n return {\n ChambersBroken: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n Scrapped: [{ product_type: lot.product_type, layer: lot.layer, priority: lot.priority, age: lot.age, defect_count: lot.defect_count, process_remaining: lot.process_remaining, wait_time: lot.wait_time, due_date: lot.due_date, process_bias: lot.process_bias, batch_wait: lot.batch_wait }],\n };\n});", + "x": 1530, + "y": 1020 + }, + { + "id": "transition__crew_reaches_broken", + "name": "Crew reaches broken chamber", + "inputArcs": [ + { + "placeId": "place__chambers_broken", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__maintenance_crew", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_in_maintenance", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => true);", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersBroken[0];\n return {\n ChambersInMaintenance: [{ condition: chamber.condition, particle_count: chamber.particle_count, hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: Distribution.Lognormal(Math.log(parameters.breakdown_repair_time), parameters.breakdown_sigma), diffusion_clock: chamber.diffusion_clock, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: chamber.process_drift, batch_count: chamber.batch_count }],\n };\n});", + "x": 2080, + "y": 1256 + }, + { + "id": "transition__drift_diffusion", + "name": "Process noise injection (diffusion step)", + "inputArcs": [ + { + "placeId": "place__chambers_processing", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__chambers_processing", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.ChambersProcessing[0].diffusion_clock <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const chamber = input.ChambersProcessing[0];\n const step = parameters.diffusion_step;\n return {\n ChambersProcessing: [{ condition: chamber.condition, particle_count: Distribution.Gaussian(Math.max(chamber.particle_count, 0), parameters.particle_volatility * Math.sqrt(step)), hours_since_maintenance: chamber.hours_since_maintenance, maintenance_remaining: chamber.maintenance_remaining, diffusion_clock: parameters.diffusion_step, lots_processed: chamber.lots_processed, machine_group: chamber.machine_group, tool_id: chamber.tool_id, chamber_idx: chamber.chamber_idx, qualification: chamber.qualification, process_drift: Distribution.Gaussian(chamber.process_drift, parameters.drift_volatility * Math.sqrt(step)), batch_count: chamber.batch_count }],\n };\n});", + "x": 1530, + "y": 810 + } + ], + "types": [ + { + "id": "type__lot", + "name": "Lot", + "iconSlug": "circle", + "displayColor": "#8b5cf6", + "elements": [ + { + "elementId": "type__lot__product_type", + "name": "product_type", + "type": "integer" + }, + { + "elementId": "type__lot__layer", + "name": "layer", + "type": "integer" + }, + { + "elementId": "type__lot__priority", + "name": "priority", + "type": "real" + }, + { + "elementId": "type__lot__age", + "name": "age", + "type": "real" + }, + { + "elementId": "type__lot__defect_count", + "name": "defect_count", + "type": "real" + }, + { + "elementId": "type__lot__process_remaining", + "name": "process_remaining", + "type": "real" + }, + { + "elementId": "type__lot__wait_time", + "name": "wait_time", + "type": "real" + }, + { + "elementId": "type__lot__due_date", + "name": "due_date", + "type": "real" + }, + { + "elementId": "type__lot__process_bias", + "name": "process_bias", + "type": "real" + }, + { + "elementId": "type__lot__batch_wait", + "name": "batch_wait", + "type": "real" + } + ] + }, + { + "id": "type__chamber", + "name": "Chamber", + "iconSlug": "circle", + "displayColor": "#06b6d4", + "elements": [ + { + "elementId": "type__chamber__condition", + "name": "condition", + "type": "real" + }, + { + "elementId": "type__chamber__particle_count", + "name": "particle_count", + "type": "real" + }, + { + "elementId": "type__chamber__hours_since_maintenance", + "name": "hours_since_maintenance", + "type": "real" + }, + { + "elementId": "type__chamber__maintenance_remaining", + "name": "maintenance_remaining", + "type": "real" + }, + { + "elementId": "type__chamber__diffusion_clock", + "name": "diffusion_clock", + "type": "real" + }, + { + "elementId": "type__chamber__lots_processed", + "name": "lots_processed", + "type": "integer" + }, + { + "elementId": "type__chamber__machine_group", + "name": "machine_group", + "type": "integer" + }, + { + "elementId": "type__chamber__tool_id", + "name": "tool_id", + "type": "integer" + }, + { + "elementId": "type__chamber__chamber_idx", + "name": "chamber_idx", + "type": "integer" + }, + { + "elementId": "type__chamber__qualification", + "name": "qualification", + "type": "integer" + }, + { + "elementId": "type__chamber__process_drift", + "name": "process_drift", + "type": "real" + }, + { + "elementId": "type__chamber__batch_count", + "name": "batch_count", + "type": "integer" + } + ] + } + ], + "differentialEquations": [ + { + "id": "de__aging", + "name": "Lot urgency escalation (+ age, wait clocks)", + "colorId": "type__lot", + "code": "export default Dynamics((tokens, parameters) => {\n return tokens.map((lot) => ({\n priority: lot.priority < parameters.max_priority\n ? (lot.priority * lot.priority) / parameters.target_cycle_time\n : 0,\n age: 1, defect_count: 0, process_remaining: 0, wait_time: 1, due_date: 0, process_bias: 0, batch_wait: 0\n }));\n});" + }, + { + "id": "de__processing", + "name": "Clock: process countdown (+ age)", + "colorId": "type__lot", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({\n priority: 0, age: 1, defect_count: 0, process_remaining: -1, wait_time: 0, due_date: 0, process_bias: 0, batch_wait: 0\n }));\n});" + }, + { + "id": "de__chamber_processing", + "name": "Chamber wear and contamination (coupled)", + "colorId": "type__chamber", + "code": "export default Dynamics((tokens, parameters) => {\n return tokens.map((chamber) => {\n const particleTarget = parameters.particle_baseline\n + parameters.particle_drift * chamber.hours_since_maintenance\n + parameters.particle_condition_factor * chamber.condition;\n return {\n condition: parameters.degradation_rate\n * (1 + chamber.particle_count / parameters.particle_threshold),\n particle_count: parameters.particle_reversion\n * (particleTarget - chamber.particle_count),\n hours_since_maintenance: 1,\n maintenance_remaining: 0,\n diffusion_clock: -1,\n process_drift: -parameters.drift_reversion * chamber.process_drift\n };\n });\n});" + }, + { + "id": "de__chamber_maintenance", + "name": "Clock: maintenance countdown", + "colorId": "type__chamber", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({\n condition: 0, particle_count: 0, hours_since_maintenance: 0, maintenance_remaining: -1, diffusion_clock: 0, process_drift: 0\n }));\n});" + }, + { + "id": "de__batch_wait", + "name": "Lot urgency escalation in batch queue (+ age, wait, batch clocks)", + "colorId": "type__lot", + "code": "export default Dynamics((tokens, parameters) => {\n return tokens.map((lot) => ({\n priority: lot.priority < parameters.max_priority\n ? (lot.priority * lot.priority) / parameters.target_cycle_time\n : 0,\n age: 1, defect_count: 0, process_remaining: 0, wait_time: 1, due_date: 0, process_bias: 0, batch_wait: 1\n }));\n});" + } + ], + "parameters": [ + { + "id": "param__litho_time", + "name": "Litho process time (hours)", + "variableName": "litho_time", + "type": "real", + "defaultValue": "2.0" + }, + { + "id": "param__etch_time", + "name": "Etch process time (hours)", + "variableName": "etch_time", + "type": "real", + "defaultValue": "1.5" + }, + { + "id": "param__furnace_time", + "name": "Furnace process time (hours)", + "variableName": "furnace_time", + "type": "real", + "defaultValue": "5.0" + }, + { + "id": "param__inspect_time", + "name": "Inspection time (hours)", + "variableName": "inspect_time", + "type": "real", + "defaultValue": "0.5" + }, + { + "id": "param__process_sigma", + "name": "Process time lognormal sigma", + "variableName": "process_sigma", + "type": "real", + "defaultValue": "0.25" + }, + { + "id": "param__degradation_rate", + "name": "Chamber degradation rate (per hour)", + "variableName": "degradation_rate", + "type": "real", + "defaultValue": "0.004" + }, + { + "id": "param__maintenance_threshold", + "name": "Condition triggering maintenance", + "variableName": "maintenance_threshold", + "type": "real", + "defaultValue": "0.85" + }, + { + "id": "param__maintenance_duration", + "name": "Maintenance duration median (hours)", + "variableName": "maintenance_duration", + "type": "real", + "defaultValue": "18" + }, + { + "id": "param__maintenance_sigma", + "name": "Maintenance duration sigma", + "variableName": "maintenance_sigma", + "type": "real", + "defaultValue": "0.35" + }, + { + "id": "param__breakdown_base_rate", + "name": "Breakdown base rate (per hour)", + "variableName": "breakdown_base_rate", + "type": "real", + "defaultValue": "0.0003" + }, + { + "id": "param__breakdown_condition_factor", + "name": "Breakdown exponential factor", + "variableName": "breakdown_condition_factor", + "type": "real", + "defaultValue": "4.0" + }, + { + "id": "param__breakdown_repair_time", + "name": "Breakdown repair median (hours)", + "variableName": "breakdown_repair_time", + "type": "real", + "defaultValue": "36" + }, + { + "id": "param__breakdown_sigma", + "name": "Breakdown repair sigma", + "variableName": "breakdown_sigma", + "type": "real", + "defaultValue": "0.4" + }, + { + "id": "param__base_defect_rate", + "name": "Base defect rate per step", + "variableName": "base_defect_rate", + "type": "real", + "defaultValue": "0.01" + }, + { + "id": "param__condition_sensitivity", + "name": "Defect sensitivity to condition", + "variableName": "condition_sensitivity", + "type": "real", + "defaultValue": "8.0" + }, + { + "id": "param__particle_sensitivity", + "name": "Defect sensitivity to particles", + "variableName": "particle_sensitivity", + "type": "real", + "defaultValue": "5.0" + }, + { + "id": "param__drift_defect_factor", + "name": "Defect sensitivity to process drift", + "variableName": "drift_defect_factor", + "type": "real", + "defaultValue": "3.0" + }, + { + "id": "param__scrap_threshold", + "name": "Cumulative defects causing scrap", + "variableName": "scrap_threshold", + "type": "real", + "defaultValue": "1.5" + }, + { + "id": "param__particle_baseline", + "name": "Particle count baseline", + "variableName": "particle_baseline", + "type": "real", + "defaultValue": "0.1" + }, + { + "id": "param__particle_drift", + "name": "Particle drift per hour since maintenance", + "variableName": "particle_drift", + "type": "real", + "defaultValue": "0.003" + }, + { + "id": "param__particle_reversion", + "name": "Particle OU reversion rate", + "variableName": "particle_reversion", + "type": "real", + "defaultValue": "0.4" + }, + { + "id": "param__particle_volatility", + "name": "Particle OU volatility", + "variableName": "particle_volatility", + "type": "real", + "defaultValue": "0.08" + }, + { + "id": "param__drift_reversion", + "name": "Process drift OU reversion rate", + "variableName": "drift_reversion", + "type": "real", + "defaultValue": "0.1" + }, + { + "id": "param__drift_volatility", + "name": "Process drift OU volatility", + "variableName": "drift_volatility", + "type": "real", + "defaultValue": "0.02" + }, + { + "id": "param__calibration_residual", + "name": "Residual drift sigma after calibration", + "variableName": "calibration_residual", + "type": "real", + "defaultValue": "0.005" + }, + { + "id": "param__diffusion_step", + "name": "Diffusion step interval (hours)", + "variableName": "diffusion_step", + "type": "real", + "defaultValue": "0.5" + }, + { + "id": "param__demand_rate", + "name": "Demand arrival rate (lots/hour)", + "variableName": "demand_rate", + "type": "real", + "defaultValue": "0.12" + }, + { + "id": "param__wip_cap", + "name": "WIP lot cap", + "variableName": "wip_cap", + "type": "integer", + "defaultValue": "50" + }, + { + "id": "param__target_cycle_time", + "name": "Target cycle time (hours)", + "variableName": "target_cycle_time", + "type": "real", + "defaultValue": "180" + }, + { + "id": "param__priority_update_interval", + "name": "Priority recalculation interval (hours)", + "variableName": "priority_update_interval", + "type": "real", + "defaultValue": "2.0" + }, + { + "id": "param__deadline_grace_period", + "name": "Hours past due before renegotiation", + "variableName": "deadline_grace_period", + "type": "real", + "defaultValue": "30" + }, + { + "id": "param__batch_size", + "name": "Furnace batch size", + "variableName": "batch_size", + "type": "integer", + "defaultValue": "4" + }, + { + "id": "param__batch_timeout", + "name": "Batch timeout (hours)", + "variableName": "batch_timeout", + "type": "real", + "defaultValue": "3.0" + }, + { + "id": "param__particle_threshold", + "name": "Particle count that doubles the wear rate", + "variableName": "particle_threshold", + "type": "real", + "defaultValue": "0.5" + }, + { + "id": "param__particle_condition_factor", + "name": "Extra particle equilibrium per unit of chamber wear", + "variableName": "particle_condition_factor", + "type": "real", + "defaultValue": "0.15" + }, + { + "id": "param__max_priority", + "name": "Priority ceiling for continuous escalation", + "variableName": "max_priority", + "type": "real", + "defaultValue": "10" + } + ], + "scenarios": [ + { + "id": "scenario__normal", + "name": "Normal operation", + "description": "16 chambers (4 litho, 6 etch, 4 furnace, 2 inspection) across 7 physical tools. Each chamber drifts independently.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "wip_cap", + "default": 50 + }, + { + "type": "real", + "identifier": "demand_rate", + "default": 0.12 + }, + { + "type": "real", + "identifier": "maintenance_threshold", + "default": 0.85 + } + ], + "parameterOverrides": { + "param__wip_cap": "scenario.wip_cap", + "param__demand_rate": "scenario.demand_rate", + "param__maintenance_threshold": "scenario.maintenance_threshold" + }, + "initialState": { + "type": "code", + "content": "return {\n FabEntrance: [],\n WIPQueue: [\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 }\n ],\n BatchQueue: [],\n InProcess: [],\n PostProcess: [],\n InInspection: [],\n Finished: [],\n Scrapped: [],\n WIPPermits: 15,\n ChambersAvailable: [\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.015, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 1, qualification: 0, process_drift: 0.0, batch_count: 0 },\n { condition: 0.32, particle_count: 0.34, hours_since_maintenance: 80.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 0, qualification: 1, process_drift: 0.005, batch_count: 0 },\n { condition: 0.4, particle_count: 0.4, hours_since_maintenance: 100.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 1, qualification: 2, process_drift: 0.01, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: 0.0, batch_count: 0 }\n ],\n ChambersProcessing: [],\n ChambersInMaintenance: [],\n ChambersBroken: [],\n MaintenanceCrew: 3,\n MaintenanceEvents: 0,\n BreakdownEvents: 0,\n LotsReleased: 0,\n LotsCompleted: 0,\n Calibrations: 0,\n};" + } + }, + { + "id": "scenario__high_drift", + "name": "High process drift", + "description": "Drift volatility doubled (0.04 vs 0.02). Chambers diverge faster from nominal, increasing defect rate and yield loss.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "wip_cap", + "default": 50 + }, + { + "type": "real", + "identifier": "demand_rate", + "default": 0.12 + }, + { + "type": "real", + "identifier": "maintenance_threshold", + "default": 0.85 + } + ], + "parameterOverrides": { + "param__wip_cap": "scenario.wip_cap", + "param__demand_rate": "scenario.demand_rate", + "param__maintenance_threshold": "scenario.maintenance_threshold", + "param__drift_volatility": "0.04" + }, + "initialState": { + "type": "code", + "content": "return {\n FabEntrance: [],\n WIPQueue: [\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 }\n ],\n BatchQueue: [],\n InProcess: [],\n PostProcess: [],\n InInspection: [],\n Finished: [],\n Scrapped: [],\n WIPPermits: 15,\n ChambersAvailable: [\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.015, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 1, qualification: 0, process_drift: 0.0, batch_count: 0 },\n { condition: 0.32, particle_count: 0.34, hours_since_maintenance: 80.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 0, qualification: 1, process_drift: 0.005, batch_count: 0 },\n { condition: 0.4, particle_count: 0.4, hours_since_maintenance: 100.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 1, qualification: 2, process_drift: 0.01, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: 0.0, batch_count: 0 }\n ],\n ChambersProcessing: [],\n ChambersInMaintenance: [],\n ChambersBroken: [],\n MaintenanceCrew: 3,\n MaintenanceEvents: 0,\n BreakdownEvents: 0,\n LotsReleased: 0,\n LotsCompleted: 0,\n Calibrations: 0,\n};" + } + }, + { + "id": "scenario__frequent_calibration", + "name": "Frequent calibration", + "description": "Maintenance threshold lowered to 0.6. Chambers are serviced more often, keeping drift low but reducing available capacity.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "wip_cap", + "default": 50 + }, + { + "type": "real", + "identifier": "demand_rate", + "default": 0.12 + }, + { + "type": "real", + "identifier": "maintenance_threshold", + "default": 0.6 + } + ], + "parameterOverrides": { + "param__wip_cap": "scenario.wip_cap", + "param__demand_rate": "scenario.demand_rate", + "param__maintenance_threshold": "scenario.maintenance_threshold" + }, + "initialState": { + "type": "code", + "content": "return {\n FabEntrance: [],\n WIPQueue: [\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 }\n ],\n BatchQueue: [],\n InProcess: [],\n PostProcess: [],\n InInspection: [],\n Finished: [],\n Scrapped: [],\n WIPPermits: 15,\n ChambersAvailable: [\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.015, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 1, qualification: 0, process_drift: 0.0, batch_count: 0 },\n { condition: 0.32, particle_count: 0.34, hours_since_maintenance: 80.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 0, qualification: 1, process_drift: 0.005, batch_count: 0 },\n { condition: 0.4, particle_count: 0.4, hours_since_maintenance: 100.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 1, qualification: 2, process_drift: 0.01, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: 0.0, batch_count: 0 }\n ],\n ChambersProcessing: [],\n ChambersInMaintenance: [],\n ChambersBroken: [],\n MaintenanceCrew: 3,\n MaintenanceEvents: 0,\n BreakdownEvents: 0,\n LotsReleased: 0,\n LotsCompleted: 0,\n Calibrations: 0,\n};" + } + }, + { + "id": "scenario__half_crew", + "name": "Reduced maintenance crew", + "description": "Only 2 technicians instead of 3. When one chamber of a multi-chamber tool is down, the other keeps running but drift accumulates.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "wip_cap", + "default": 50 + }, + { + "type": "real", + "identifier": "demand_rate", + "default": 0.12 + }, + { + "type": "real", + "identifier": "maintenance_threshold", + "default": 0.85 + } + ], + "parameterOverrides": { + "param__wip_cap": "scenario.wip_cap", + "param__demand_rate": "scenario.demand_rate", + "param__maintenance_threshold": "scenario.maintenance_threshold" + }, + "initialState": { + "type": "code", + "content": "return {\n FabEntrance: [],\n WIPQueue: [\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 },\n { product_type: 2, layer: 21, priority: 1.0, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.063, batch_wait: 0 },\n { product_type: 0, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.0, batch_wait: 0 },\n { product_type: 1, layer: 7, priority: 1.11, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.021, batch_wait: 0 },\n { product_type: 2, layer: 14, priority: 1.0, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.042, batch_wait: 0 },\n { product_type: 0, layer: 21, priority: 1.22, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.063, batch_wait: 0 },\n { product_type: 1, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.0, batch_wait: 0 },\n { product_type: 2, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.021, batch_wait: 0 },\n { product_type: 0, layer: 14, priority: 1.09, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.042, batch_wait: 0 },\n { product_type: 1, layer: 21, priority: 1.41, age: 52.5, defect_count: 0.378, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.063, batch_wait: 0 },\n { product_type: 2, layer: 0, priority: 1.0, age: 0.0, defect_count: 0.0, process_remaining: 0, wait_time: 0, due_date: 250, process_bias: 0.0, batch_wait: 0 },\n { product_type: 0, layer: 7, priority: 1.0, age: 17.5, defect_count: 0.126, process_remaining: 0, wait_time: 0, due_date: 200, process_bias: 0.021, batch_wait: 0 },\n { product_type: 1, layer: 14, priority: 1.24, age: 35.0, defect_count: 0.252, process_remaining: 0, wait_time: 0, due_date: 180, process_bias: 0.042, batch_wait: 0 }\n ],\n BatchQueue: [],\n InProcess: [],\n PostProcess: [],\n InInspection: [],\n Finished: [],\n Scrapped: [],\n WIPPermits: 15,\n ChambersAvailable: [\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 0, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.015, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 1, chamber_idx: 1, qualification: 0, process_drift: 0.0, batch_count: 0 },\n { condition: 0.32, particle_count: 0.34, hours_since_maintenance: 80.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 0, qualification: 1, process_drift: 0.005, batch_count: 0 },\n { condition: 0.4, particle_count: 0.4, hours_since_maintenance: 100.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 1, tool_id: 2, chamber_idx: 1, qualification: 2, process_drift: 0.01, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.01, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 0, chamber_idx: 1, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.16, particle_count: 0.22, hours_since_maintenance: 40.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 0, qualification: 1, process_drift: 0.0, batch_count: 0 },\n { condition: 0.24, particle_count: 0.28, hours_since_maintenance: 60.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 2, tool_id: 1, chamber_idx: 1, qualification: 2, process_drift: 0.005, batch_count: 0 },\n { condition: 0.0, particle_count: 0.1, hours_since_maintenance: 0.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 0, chamber_idx: 0, qualification: 0, process_drift: -0.005, batch_count: 0 },\n { condition: 0.08, particle_count: 0.16, hours_since_maintenance: 20.0, maintenance_remaining: 0, diffusion_clock: 0, lots_processed: 0, machine_group: 3, tool_id: 1, chamber_idx: 0, qualification: 0, process_drift: 0.0, batch_count: 0 }\n ],\n ChambersProcessing: [],\n ChambersInMaintenance: [],\n ChambersBroken: [],\n MaintenanceCrew: 2,\n MaintenanceEvents: 0,\n BreakdownEvents: 0,\n LotsReleased: 0,\n LotsCompleted: 0,\n Calibrations: 0,\n};" + } + } + ], + "metrics": [ + { + "id": "metric__throughput", + "name": "Throughput", + "description": "Lots completing all layers.", + "code": "return state.places.Finished.count;" + }, + { + "id": "metric__yield", + "name": "Yield", + "description": "Fraction of exiting lots that pass.", + "code": "const finished = state.places.Finished.count;\nconst scrapped = state.places.Scrapped.count;\nconst total = finished + scrapped;\nreturn total === 0 ? 1 : finished / total;" + }, + { + "id": "metric__avg_cycle_time", + "name": "Average cycle time (hours)", + "description": "Mean age of finished lots.", + "code": "const lots = state.places.Finished.tokens;\nif (lots.length === 0) return 0;\nreturn lots.reduce((sum, lot) => sum + lot.age, 0) / lots.length;" + }, + { + "id": "metric__on_time_delivery", + "name": "On-time delivery rate", + "description": "Fraction of finished lots within original due date.", + "code": "const lots = state.places.Finished.tokens;\nif (lots.length === 0) return 1;\nconst onTime = lots.reduce((n, lot) => lot.age <= lot.due_date ? n + 1 : n, 0);\nreturn onTime / lots.length;" + }, + { + "id": "metric__avg_process_bias", + "name": "Average process bias", + "description": "Mean absolute accumulated drift across finished lots.", + "code": "const lots = state.places.Finished.tokens;\nif (lots.length === 0) return 0;\nreturn lots.reduce((sum, lot) => sum + Math.abs(lot.process_bias), 0) / lots.length;" + }, + { + "id": "metric__max_drift", + "name": "Maximum chamber drift", + "description": "Worst-case absolute process drift across all active chambers.", + "code": "const all = state.places.ChambersAvailable.tokens.concat(state.places.ChambersProcessing.tokens);\nif (all.length === 0) return 0;\nreturn all.reduce((mx, c) => Math.max(mx, Math.abs(c.process_drift)), 0);" + }, + { + "id": "metric__chamber_utilisation", + "name": "Chamber utilisation", + "description": "Fraction of chambers currently processing.", + "code": "const processing = state.places.ChambersProcessing.count;\nconst available = state.places.ChambersAvailable.count;\nconst inMaint = state.places.ChambersInMaintenance.count;\nconst broken = state.places.ChambersBroken.count;\nconst total = processing + available + inMaint + broken;\nreturn total === 0 ? 0 : processing / total;" + }, + { + "id": "metric__maintenance_events", + "name": "Maintenance events", + "description": "Cumulative maintenance starts.", + "code": "return state.places.MaintenanceEvents.count;" + }, + { + "id": "metric__breakdowns", + "name": "Unplanned breakdowns", + "description": "Cumulative breakdowns.", + "code": "return state.places.BreakdownEvents.count;" + }, + { + "id": "metric__calibrations", + "name": "Calibrations", + "description": "Drift recalibrations (completed maintenance).", + "code": "return state.places.Calibrations.count;" + }, + { + "id": "metric__wip_level", + "name": "WIP level", + "description": "Lots currently in the fab.", + "code": "return state.places.WIPQueue.count + state.places.InProcess.count + state.places.InInspection.count + state.places.PostProcess.count + state.places.BatchQueue.count;" + }, + { + "id": "metric__batch_queue_size", + "name": "Batch queue size", + "description": "Lots waiting for furnace batch.", + "code": "return state.places.BatchQueue.count;" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Semiconductor fab \u2014 process drift & multi-chamber tools (v2)" +} diff --git a/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/truck-fleet-predictive-maintenance-layout.json b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/truck-fleet-predictive-maintenance-layout.json new file mode 100644 index 00000000000..e6757beb051 --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/truck-fleet-predictive-maintenance-layout.json @@ -0,0 +1,2235 @@ +{ + "places": [ + { + "id": "place__load_board", + "name": "LoadBoard", + "colorId": "type__load", + "dynamicsEnabled": true, + "differentialEquationId": "de__waiting_load", + "showAsInitialState": false, + "x": 540, + "y": 1530 + }, + { + "id": "place__available", + "name": "Available", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__stopped", + "showAsInitialState": true, + "x": 3660, + "y": 675 + }, + { + "id": "place__drivers", + "name": "Drivers", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2685, + "y": 1575 + }, + { + "id": "place__on_route", + "name": "OnRoute", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__driving", + "showAsInitialState": false, + "x": 1035, + "y": 1290 + }, + { + "id": "place__stranded", + "name": "Stranded", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__stopped", + "showAsInitialState": false, + "x": 1605, + "y": 1395 + }, + { + "id": "place__recovery", + "name": "RecoveryUnits", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2145, + "y": 1620 + }, + { + "id": "place__under_recovery", + "name": "UnderRecovery", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__stopped", + "showAsInitialState": false, + "x": 2160, + "y": 1395 + }, + { + "id": "place__returning", + "name": "Returning", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__returning", + "showAsInitialState": false, + "x": 1605, + "y": 870 + }, + { + "id": "place__depot_queue", + "name": "DepotQueue", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__stopped", + "showAsInitialState": false, + "x": 2115, + "y": 645 + }, + { + "id": "place__bays", + "name": "Bays", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2115, + "y": 480 + }, + { + "id": "place__technicians", + "name": "Technicians", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2115, + "y": 285 + }, + { + "id": "place__spares", + "name": "Spares", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": true, + "x": 2115, + "y": 135 + }, + { + "id": "place__in_bay", + "name": "InBay", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__in_bay", + "showAsInitialState": false, + "x": 3135, + "y": 150 + }, + { + "id": "place__needs_repair", + "name": "NeedsRepair", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__stopped", + "showAsInitialState": false, + "x": 2115, + "y": -15 + }, + { + "id": "place__awaiting_parts", + "name": "AwaitingParts", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__stopped", + "showAsInitialState": false, + "x": 2670, + "y": 150 + }, + { + "id": "place__parts_on_order", + "name": "PartsOnOrder", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2670, + "y": 375 + }, + { + "id": "place__delivered", + "name": "DeliveredLoads", + "colorId": "type__load", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1605, + "y": 1170 + }, + { + "id": "place__late_loads", + "name": "LateLoads", + "colorId": "type__load", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1605, + "y": 1020 + }, + { + "id": "place__dropped_loads", + "name": "DroppedLoads", + "colorId": "type__load", + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1605, + "y": 1785 + }, + { + "id": "place__roadside_events", + "name": "RoadsideEvents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 1605, + "y": 1620 + }, + { + "id": "place__services_done", + "name": "ServicesDone", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 3645, + "y": 0 + }, + { + "id": "place__repairs_done", + "name": "RepairsDone", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 3660, + "y": 285 + }, + { + "id": "place__deferred", + "name": "DeferredServices", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2670, + "y": 570 + }, + { + "id": "place__conditions", + "name": "Conditions", + "colorId": "type__conditions", + "dynamicsEnabled": true, + "differentialEquationId": "de__conditions", + "showAsInitialState": true, + "x": 750, + "y": 1080 + }, + { + "id": "place__rest", + "name": "Rest", + "colorId": "type__truck", + "dynamicsEnabled": true, + "differentialEquationId": "de__resting", + "showAsInitialState": false, + "x": 2670, + "y": 915 + }, + { + "id": "place__rest_events", + "name": "RestEvents", + "colorId": null, + "dynamicsEnabled": false, + "differentialEquationId": null, + "showAsInitialState": false, + "x": 2670, + "y": 1065 + } + ], + "transitions": [ + { + "id": "transition__load_motorway", + "name": "A motorway load is offered", + "inputArcs": [], + "outputArcs": [ + { + "placeId": "place__load_board", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.motorway_rate;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n LoadBoard: [\n {\n route_class: 0,\n distance: 420,\n due: 420 / parameters.average_speed * parameters.due_allowance,\n revenue: 420 * parameters.revenue_per_km,\n age: 0,\n },\n ],\n };\n});", + "x": 300, + "y": 1425 + }, + { + "id": "transition__load_urban", + "name": "A urban load is offered", + "inputArcs": [], + "outputArcs": [ + { + "placeId": "place__load_board", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.urban_rate;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n LoadBoard: [\n {\n route_class: 1,\n distance: 180,\n due: 180 / parameters.average_speed * parameters.due_allowance,\n revenue: 180 * parameters.revenue_per_km,\n age: 0,\n },\n ],\n };\n});", + "x": 300, + "y": 1530 + }, + { + "id": "transition__load_mountain", + "name": "A mountain load is offered", + "inputArcs": [], + "outputArcs": [ + { + "placeId": "place__load_board", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return parameters.mountain_rate;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n return {\n LoadBoard: [\n {\n route_class: 2,\n distance: 260,\n due: 260 / parameters.average_speed * parameters.due_allowance,\n revenue: 260 * parameters.revenue_per_km,\n age: 0,\n },\n ],\n };\n});", + "x": 300, + "y": 1635 + }, + { + "id": "transition__dispatch", + "name": "Dispatch a truck", + "inputArcs": [ + { + "placeId": "place__load_board", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__available", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__drivers", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__conditions", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__on_route", + "weight": 1 + }, + { + "placeId": "place__conditions", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const truck = input.Available[0];\n const load = input.LoadBoard[0];\n const limit = load.route_class === 2\n ? parameters.severe_route_wear_limit\n : parameters.wear_limit;\n const maxWear = Math.max(truck.brake_wear, truck.engine_wear, truck.tyre_wear);\n const estimatedHours = load.distance / (parameters.average_speed * 0.9);\n return maxWear < limit\n && truck.hours_driven + estimatedHours < parameters.max_driving_hours * 1.5;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.Available[0];\n const load = input.LoadBoard[0];\n const cond = input.Conditions[0];\n return {\n OnRoute: [{\n brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: load.distance, route_distance: load.distance, service_remaining: truck.service_remaining, route_class: load.route_class, load_due: truck.age + load.due - load.age, load_revenue: load.revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: cond.severity_mean, speed_factor: cond.speed_mean, conditions_clock: parameters.conditions_step, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: parameters.fuel_per_km\n }],\n Conditions: [{ severity_mean: cond.severity_mean, speed_mean: cond.speed_mean, clock: cond.clock }],\n };\n});", + "x": 765, + "y": 1290 + }, + { + "id": "transition__deliver_on_time", + "name": "Load delivered on time", + "inputArcs": [ + { + "placeId": "place__on_route", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__returning", + "weight": 1 + }, + { + "placeId": "place__delivered", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const truck = input.OnRoute[0];\n return truck.km_remaining <= 0 && truck.age <= truck.load_due;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.OnRoute[0];\n return {\n Returning: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.route_distance, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done + 1, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: parameters.conditions_step, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }],\n DeliveredLoads: [{ route_class: truck.route_class, distance: truck.route_distance, due: truck.load_due, revenue: truck.load_revenue, age: truck.age }],\n };\n});", + "x": 1305, + "y": 1170 + }, + { + "id": "transition__deliver_late", + "name": "Load delivered late", + "inputArcs": [ + { + "placeId": "place__on_route", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__returning", + "weight": 1 + }, + { + "placeId": "place__late_loads", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const truck = input.OnRoute[0];\n return truck.km_remaining <= 0 && truck.age > truck.load_due;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.OnRoute[0];\n return {\n Returning: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.route_distance, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done + 1, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: parameters.conditions_step, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }],\n LateLoads: [{ route_class: truck.route_class, distance: truck.route_distance, due: truck.load_due, revenue: truck.load_revenue, age: truck.age }],\n };\n});", + "x": 1305, + "y": 960 + }, + { + "id": "transition__breakdown", + "name": "Truck fails at the roadside", + "inputArcs": [ + { + "placeId": "place__on_route", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__stranded", + "weight": 1 + }, + { + "placeId": "place__roadside_events", + "weight": 1 + }, + { + "placeId": "place__dropped_loads", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n const truck = input.OnRoute[0];\n const routeSeverity = truck.route_class === 0 ? 1.0\n : truck.route_class === 1 ? 1.8 : 3.2;\n const brakeHazard = 1 + parameters.brake_sensitivity * truck.brake_wear;\n const engineHazard = 1 + parameters.engine_sensitivity * truck.engine_wear;\n const tyreHazard = 1 + parameters.tyre_sensitivity * truck.tyre_wear;\n return parameters.failure_rate * routeSeverity\n * Math.max(brakeHazard, engineHazard, tyreHazard);\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.OnRoute[0];\n return {\n Stranded: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }],\n DroppedLoads: [{ route_class: truck.route_class, distance: truck.route_distance, due: truck.load_due, revenue: truck.load_revenue, age: truck.age }],\n };\n});", + "x": 1305, + "y": 1395 + }, + { + "id": "transition__recover", + "name": "Recovery unit reaches the truck", + "inputArcs": [ + { + "placeId": "place__stranded", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__recovery", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__under_recovery", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / parameters.recovery_response;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.Stranded[0];\n return { UnderRecovery: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 1890, + "y": 1395 + }, + { + "id": "transition__tow_home", + "name": "Truck towed back to the depot", + "inputArcs": [ + { + "placeId": "place__under_recovery", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__needs_repair", + "weight": 1 + }, + { + "placeId": "place__recovery", + "weight": 1 + }, + { + "placeId": "place__drivers", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / parameters.tow_time;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.UnderRecovery[0];\n return { NeedsRepair: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: 0, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2430, + "y": 1395 + }, + { + "id": "transition__start_repair", + "name": "Repair after a breakdown starts", + "inputArcs": [ + { + "placeId": "place__needs_repair", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__bays", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__technicians", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__spares", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_bay", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => true);", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.NeedsRepair[0];\n return { InBay: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: Distribution.Lognormal(Math.log(parameters.repair_time), 0.4), route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: 1, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2415, + "y": -165 + }, + { + "id": "transition__repair_waits_for_parts", + "name": "Repair waits for a part", + "inputArcs": [ + { + "placeId": "place__needs_repair", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__bays", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__technicians", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__spares", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__awaiting_parts", + "weight": 1 + }, + { + "placeId": "place__parts_on_order", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => true);", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.NeedsRepair[0];\n return { AwaitingParts: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: 1, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2415, + "y": 45 + }, + { + "id": "transition__arrive_depot", + "name": "Truck arrives back at the depot", + "inputArcs": [ + { + "placeId": "place__returning", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__depot_queue", + "weight": 1 + }, + { + "placeId": "place__drivers", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.Returning[0].km_remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.Returning[0];\n return { DepotQueue: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: 0, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 1875, + "y": 870 + }, + { + "id": "transition__park", + "name": "Truck parks up, no service due", + "inputArcs": [ + { + "placeId": "place__depot_queue", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__available", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const truck = input.DepotQueue[0];\n const maxWear = Math.max(truck.brake_wear, truck.engine_wear, truck.tyre_wear);\n return maxWear < parameters.service_wear_limit\n && truck.hours_driven < parameters.max_driving_hours;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.DepotQueue[0];\n return { Available: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2415, + "y": 795 + }, + { + "id": "transition__park_workshop_full", + "name": "Service deferred, workshop full", + "inputArcs": [ + { + "placeId": "place__depot_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__bays", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__available", + "weight": 1 + }, + { + "placeId": "place__deferred", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const truck = input.DepotQueue[0];\n const maxWear = Math.max(truck.brake_wear, truck.engine_wear, truck.tyre_wear);\n return maxWear >= parameters.service_wear_limit;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.DepotQueue[0];\n return { Available: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2415, + "y": 645 + }, + { + "id": "transition__into_bay", + "name": "Truck goes into a bay", + "inputArcs": [ + { + "placeId": "place__depot_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__bays", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__technicians", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__spares", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_bay", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const truck = input.DepotQueue[0];\n const maxWear = Math.max(truck.brake_wear, truck.engine_wear, truck.tyre_wear);\n return maxWear >= parameters.service_wear_limit;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.DepotQueue[0];\n return { InBay: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: Distribution.Lognormal(Math.log(parameters.service_time), 0.3), route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: 0, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2400, + "y": 435 + }, + { + "id": "transition__wait_for_parts", + "name": "Truck waits for a part", + "inputArcs": [ + { + "placeId": "place__depot_queue", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__bays", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__technicians", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__spares", + "weight": 1, + "type": "inhibitor" + } + ], + "outputArcs": [ + { + "placeId": "place__awaiting_parts", + "weight": 1 + }, + { + "placeId": "place__parts_on_order", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n const truck = input.DepotQueue[0];\n const maxWear = Math.max(truck.brake_wear, truck.engine_wear, truck.tyre_wear);\n return maxWear >= parameters.service_wear_limit;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.DepotQueue[0];\n return { AwaitingParts: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: 0, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2415, + "y": 270 + }, + { + "id": "transition__parts_arrive", + "name": "Ordered part arrives", + "inputArcs": [ + { + "placeId": "place__parts_on_order", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__spares", + "weight": 1 + } + ], + "lambdaType": "stochastic", + "lambdaCode": "export default Lambda((input, parameters) => {\n return 1 / parameters.parts_lead_time;\n});", + "transitionKernelCode": "", + "x": 2895, + "y": 375 + }, + { + "id": "transition__fit_part", + "name": "Part fitted", + "inputArcs": [ + { + "placeId": "place__awaiting_parts", + "weight": 1, + "type": "standard" + }, + { + "placeId": "place__spares", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__in_bay", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda(() => true);", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.AwaitingParts[0];\n const duration = truck.unplanned === 1\n ? Distribution.Lognormal(Math.log(parameters.repair_time), 0.4)\n : Distribution.Lognormal(Math.log(parameters.service_time), 0.3);\n return { InBay: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: duration, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2895, + "y": -90 + }, + { + "id": "transition__service_complete", + "name": "Planned service finished", + "inputArcs": [ + { + "placeId": "place__in_bay", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__available", + "weight": 1 + }, + { + "placeId": "place__bays", + "weight": 1 + }, + { + "placeId": "place__technicians", + "weight": 1 + }, + { + "placeId": "place__services_done", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const truck = input.InBay[0];\n return truck.service_remaining <= 0 && truck.unplanned === 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.InBay[0];\n return { Available: [{ brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: 0, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 3375, + "y": 0 + }, + { + "id": "transition__repair_complete", + "name": "Breakdown repair finished", + "inputArcs": [ + { + "placeId": "place__in_bay", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__available", + "weight": 1 + }, + { + "placeId": "place__bays", + "weight": 1 + }, + { + "placeId": "place__technicians", + "weight": 1 + }, + { + "placeId": "place__repairs_done", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n const truck = input.InBay[0];\n return truck.service_remaining <= 0 && truck.unplanned === 1;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.InBay[0];\n return { Available: [{ brake_wear: truck.brake_wear * 0.5, engine_wear: truck.engine_wear * 0.5, tyre_wear: truck.tyre_wear * 0.5, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: 0, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 3375, + "y": 285 + }, + { + "id": "transition__load_expires", + "name": "Load goes to another haulier", + "inputArcs": [ + { + "placeId": "place__load_board", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__dropped_loads", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.LoadBoard[0].age > parameters.board_patience;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const load = input.LoadBoard[0];\n return { DroppedLoads: [{ route_class: load.route_class, distance: load.distance, due: load.due, revenue: load.revenue, age: load.age }] };\n});", + "x": 915, + "y": 1785 + }, + { + "id": "transition__conditions_on_route", + "name": "Road noise injection (OnRoute)", + "inputArcs": [ + { + "placeId": "place__on_route", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__on_route", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.OnRoute[0].conditions_clock <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.OnRoute[0];\n const step = parameters.conditions_step;\n return {\n OnRoute: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: Distribution.Gaussian(truck.road_severity, parameters.severity_volatility * Math.sqrt(step)), speed_factor: Distribution.Gaussian(truck.speed_factor, parameters.speed_volatility * Math.sqrt(step)), conditions_clock: parameters.conditions_step, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }],\n };\n});", + "x": 1305, + "y": 1620 + }, + { + "id": "transition__conditions_returning", + "name": "Road noise injection (Returning)", + "inputArcs": [ + { + "placeId": "place__returning", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__returning", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.Returning[0].conditions_clock <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.Returning[0];\n const step = parameters.conditions_step;\n return {\n Returning: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: Distribution.Gaussian(truck.road_severity, parameters.severity_volatility * Math.sqrt(step)), speed_factor: Distribution.Gaussian(truck.speed_factor, parameters.speed_volatility * Math.sqrt(step)), conditions_clock: parameters.conditions_step, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }],\n };\n});", + "x": 1620, + "y": 705 + }, + { + "id": "transition__env_shift", + "name": "Weather noise injection", + "inputArcs": [ + { + "placeId": "place__conditions", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__conditions", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.Conditions[0].clock <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const cond = input.Conditions[0];\n const step = parameters.env_step;\n return {\n Conditions: [{ severity_mean: Distribution.Gaussian(cond.severity_mean, parameters.env_volatility * Math.sqrt(step)), speed_mean: Distribution.Gaussian(cond.speed_mean, parameters.env_volatility * Math.sqrt(step)), clock: parameters.env_step }],\n };\n});", + "x": 765, + "y": 885 + }, + { + "id": "transition__mandatory_rest", + "name": "Mandatory rest", + "inputArcs": [ + { + "placeId": "place__depot_queue", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__rest", + "weight": 1 + }, + { + "placeId": "place__rest_events", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input, parameters) => {\n return input.DepotQueue[0].hours_driven >= parameters.max_driving_hours;\n});", + "transitionKernelCode": "export default TransitionKernel((input, parameters) => {\n const truck = input.DepotQueue[0];\n return {\n Rest: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: truck.hours_driven, rest_remaining: Distribution.Lognormal(Math.log(11), 0.15), fuel_rate: truck.fuel_rate }],\n };\n});", + "x": 2415, + "y": 915 + }, + { + "id": "transition__rest_complete", + "name": "Rest complete", + "inputArcs": [ + { + "placeId": "place__rest", + "weight": 1, + "type": "standard" + } + ], + "outputArcs": [ + { + "placeId": "place__available", + "weight": 1 + } + ], + "lambdaType": "predicate", + "lambdaCode": "export default Lambda((input) => {\n return input.Rest[0].rest_remaining <= 0;\n});", + "transitionKernelCode": "export default TransitionKernel((input) => {\n const truck = input.Rest[0];\n return { Available: [{ brake_wear: truck.brake_wear, engine_wear: truck.engine_wear, tyre_wear: truck.tyre_wear, km_remaining: truck.km_remaining, route_distance: truck.route_distance, service_remaining: truck.service_remaining, route_class: truck.route_class, load_due: truck.load_due, load_revenue: truck.load_revenue, age: truck.age, loads_done: truck.loads_done, unplanned: truck.unplanned, road_severity: truck.road_severity, speed_factor: truck.speed_factor, conditions_clock: truck.conditions_clock, fuel_burned: truck.fuel_burned, hours_driven: 0, rest_remaining: truck.rest_remaining, fuel_rate: truck.fuel_rate }] };\n});", + "x": 2895, + "y": 915 + } + ], + "types": [ + { + "id": "type__truck", + "name": "Truck", + "iconSlug": "circle", + "displayColor": "#3b82f6", + "elements": [ + { + "elementId": "type__truck__brake_wear", + "name": "brake_wear", + "type": "real" + }, + { + "elementId": "type__truck__engine_wear", + "name": "engine_wear", + "type": "real" + }, + { + "elementId": "type__truck__tyre_wear", + "name": "tyre_wear", + "type": "real" + }, + { + "elementId": "type__truck__km_remaining", + "name": "km_remaining", + "type": "real" + }, + { + "elementId": "type__truck__route_distance", + "name": "route_distance", + "type": "real" + }, + { + "elementId": "type__truck__service_remaining", + "name": "service_remaining", + "type": "real" + }, + { + "elementId": "type__truck__route_class", + "name": "route_class", + "type": "integer" + }, + { + "elementId": "type__truck__load_due", + "name": "load_due", + "type": "real" + }, + { + "elementId": "type__truck__load_revenue", + "name": "load_revenue", + "type": "real" + }, + { + "elementId": "type__truck__age", + "name": "age", + "type": "real" + }, + { + "elementId": "type__truck__loads_done", + "name": "loads_done", + "type": "integer" + }, + { + "elementId": "type__truck__unplanned", + "name": "unplanned", + "type": "integer" + }, + { + "elementId": "type__truck__road_severity", + "name": "road_severity", + "type": "real" + }, + { + "elementId": "type__truck__speed_factor", + "name": "speed_factor", + "type": "real" + }, + { + "elementId": "type__truck__conditions_clock", + "name": "conditions_clock", + "type": "real" + }, + { + "elementId": "type__truck__fuel_burned", + "name": "fuel_burned", + "type": "real" + }, + { + "elementId": "type__truck__hours_driven", + "name": "hours_driven", + "type": "real" + }, + { + "elementId": "type__truck__rest_remaining", + "name": "rest_remaining", + "type": "real" + }, + { + "elementId": "type__truck__fuel_rate", + "name": "fuel_rate", + "type": "real" + } + ] + }, + { + "id": "type__load", + "name": "Load", + "iconSlug": "circle", + "displayColor": "#f97316", + "elements": [ + { + "elementId": "type__load__route_class", + "name": "route_class", + "type": "integer" + }, + { + "elementId": "type__load__distance", + "name": "distance", + "type": "real" + }, + { + "elementId": "type__load__due", + "name": "due", + "type": "real" + }, + { + "elementId": "type__load__revenue", + "name": "revenue", + "type": "real" + }, + { + "elementId": "type__load__age", + "name": "age", + "type": "real" + } + ] + }, + { + "id": "type__conditions", + "name": "Conditions", + "iconSlug": "circle", + "displayColor": "#10b981", + "elements": [ + { + "elementId": "type__conditions__severity_mean", + "name": "severity_mean", + "type": "real" + }, + { + "elementId": "type__conditions__speed_mean", + "name": "speed_mean", + "type": "real" + }, + { + "elementId": "type__conditions__clock", + "name": "clock", + "type": "real" + } + ] + } + ], + "differentialEquations": [ + { + "id": "de__driving", + "name": "Wear, fuel and road conditions (loaded)", + "colorId": "type__truck", + "code": "export default Dynamics((tokens, parameters) => {\n return tokens.map((truck) => {\n const speed = parameters.average_speed * truck.speed_factor;\n const severity = truck.road_severity;\n const brakeRoute = truck.route_class === 2 ? 2.5 : truck.route_class === 1 ? 1.4 : 1.0;\n const tyreRoute = truck.route_class === 2 ? 1.6 : 1.0;\n return {\n brake_wear: parameters.brake_wear_per_km * speed * severity * brakeRoute\n * (1 + parameters.wear_feedback * truck.brake_wear),\n engine_wear: parameters.engine_wear_per_km * speed * severity * 1.2\n * (1 + parameters.wear_feedback * truck.engine_wear),\n tyre_wear: parameters.tyre_wear_per_km * speed * severity * tyreRoute\n * (1 + parameters.wear_feedback * truck.tyre_wear),\n km_remaining: -speed,\n route_distance: 0, service_remaining: 0, load_due: 0, load_revenue: 0, age: 1,\n road_severity: parameters.severity_reversion\n * (parameters.base_severity_mean - truck.road_severity),\n speed_factor: parameters.speed_reversion\n * (parameters.base_speed_mean - truck.speed_factor),\n conditions_clock: -1,\n fuel_burned: parameters.fuel_per_km * speed * severity\n * (truck.route_class === 2 ? 1.4 : 1.0),\n hours_driven: 1, rest_remaining: 0, fuel_rate: 0\n };\n });\n});" + }, + { + "id": "de__returning", + "name": "Wear, fuel and road conditions (running back empty)", + "colorId": "type__truck", + "code": "export default Dynamics((tokens, parameters) => {\n return tokens.map((truck) => {\n const speed = parameters.average_speed * truck.speed_factor;\n const severity = truck.road_severity;\n const brakeRoute = truck.route_class === 2 ? 2.5 : truck.route_class === 1 ? 1.4 : 1.0;\n const tyreRoute = truck.route_class === 2 ? 1.6 : 1.0;\n return {\n brake_wear: parameters.brake_wear_per_km * speed * severity * brakeRoute * 0.7\n * (1 + parameters.wear_feedback * truck.brake_wear),\n engine_wear: parameters.engine_wear_per_km * speed * severity * 0.7\n * (1 + parameters.wear_feedback * truck.engine_wear),\n tyre_wear: parameters.tyre_wear_per_km * speed * severity * tyreRoute * 0.7\n * (1 + parameters.wear_feedback * truck.tyre_wear),\n km_remaining: -speed,\n route_distance: 0, service_remaining: 0, load_due: 0, load_revenue: 0, age: 1,\n road_severity: parameters.severity_reversion\n * (parameters.base_severity_mean - truck.road_severity),\n speed_factor: parameters.speed_reversion\n * (parameters.base_speed_mean - truck.speed_factor),\n conditions_clock: -1,\n fuel_burned: parameters.fuel_per_km * speed * severity\n * (truck.route_class === 2 ? 1.4 : 1.0) * 0.8,\n hours_driven: 1, rest_remaining: 0, fuel_rate: 0\n };\n });\n});" + }, + { + "id": "de__stopped", + "name": "Clock: standing at the depot (age only)", + "colorId": "type__truck", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({ brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, load_due: 0, load_revenue: 0, age: 1, road_severity: 0, speed_factor: 0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0 }));\n});" + }, + { + "id": "de__in_bay", + "name": "Clock: service countdown", + "colorId": "type__truck", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({ brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: -1, load_due: 0, load_revenue: 0, age: 1, road_severity: 0, speed_factor: 0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0 }));\n});" + }, + { + "id": "de__resting", + "name": "Clock: driver rest countdown", + "colorId": "type__truck", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({ brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, load_due: 0, load_revenue: 0, age: 1, road_severity: 0, speed_factor: 0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: -1, fuel_rate: 0 }));\n});" + }, + { + "id": "de__waiting_load", + "name": "Clock: load ageing on the board", + "colorId": "type__load", + "code": "export default Dynamics((tokens) => {\n return tokens.map(() => ({ distance: 0, due: 0, revenue: 0, age: 1 }));\n});" + }, + { + "id": "de__conditions", + "name": "Regional weather drift (+ resample clock)", + "colorId": "type__conditions", + "code": "export default Dynamics((tokens, parameters) => {\n return tokens.map((cond) => ({\n severity_mean: parameters.env_reversion\n * (parameters.base_severity_mean - cond.severity_mean),\n speed_mean: parameters.env_reversion\n * (parameters.base_speed_mean - cond.speed_mean),\n clock: -1\n }));\n});" + } + ], + "parameters": [ + { + "id": "param__average_speed", + "name": "Average speed (km per hour)", + "variableName": "average_speed", + "type": "real", + "defaultValue": "62" + }, + { + "id": "param__motorway_rate", + "name": "Motorway loads offered (per hour)", + "variableName": "motorway_rate", + "type": "real", + "defaultValue": "0.07" + }, + { + "id": "param__urban_rate", + "name": "Urban loads offered (per hour)", + "variableName": "urban_rate", + "type": "real", + "defaultValue": "0.095" + }, + { + "id": "param__mountain_rate", + "name": "Mountain loads offered (per hour)", + "variableName": "mountain_rate", + "type": "real", + "defaultValue": "0.048" + }, + { + "id": "param__due_allowance", + "name": "Delivery window as a multiple of driving time", + "variableName": "due_allowance", + "type": "real", + "defaultValue": "2.2" + }, + { + "id": "param__revenue_per_km", + "name": "Revenue per km", + "variableName": "revenue_per_km", + "type": "real", + "defaultValue": "1.4" + }, + { + "id": "param__board_patience", + "name": "Hours a load stays on the board", + "variableName": "board_patience", + "type": "real", + "defaultValue": "10" + }, + { + "id": "param__brake_wear_per_km", + "name": "Brake wear per km", + "variableName": "brake_wear_per_km", + "type": "real", + "defaultValue": "0.000015" + }, + { + "id": "param__engine_wear_per_km", + "name": "Engine wear per km", + "variableName": "engine_wear_per_km", + "type": "real", + "defaultValue": "0.000012" + }, + { + "id": "param__tyre_wear_per_km", + "name": "Tyre wear per km", + "variableName": "tyre_wear_per_km", + "type": "real", + "defaultValue": "0.000018" + }, + { + "id": "param__failure_rate", + "name": "Roadside failure rate for a new truck (per hour)", + "variableName": "failure_rate", + "type": "real", + "defaultValue": "0.00035" + }, + { + "id": "param__brake_sensitivity", + "name": "How much brake wear multiplies the failure rate", + "variableName": "brake_sensitivity", + "type": "real", + "defaultValue": "50" + }, + { + "id": "param__engine_sensitivity", + "name": "How much engine wear multiplies the failure rate", + "variableName": "engine_sensitivity", + "type": "real", + "defaultValue": "70" + }, + { + "id": "param__tyre_sensitivity", + "name": "How much tyre wear multiplies the failure rate", + "variableName": "tyre_sensitivity", + "type": "real", + "defaultValue": "40" + }, + { + "id": "param__service_wear_limit", + "name": "Wear level that sends a truck into a bay", + "variableName": "service_wear_limit", + "type": "real", + "defaultValue": "0.5" + }, + { + "id": "param__wear_limit", + "name": "Wear a truck may carry onto an ordinary route", + "variableName": "wear_limit", + "type": "real", + "defaultValue": "9" + }, + { + "id": "param__severe_route_wear_limit", + "name": "Wear a truck may carry onto a mountain route", + "variableName": "severe_route_wear_limit", + "type": "real", + "defaultValue": "9" + }, + { + "id": "param__service_time", + "name": "Service duration (hours)", + "variableName": "service_time", + "type": "real", + "defaultValue": "5" + }, + { + "id": "param__parts_lead_time", + "name": "Parts lead time (hours)", + "variableName": "parts_lead_time", + "type": "real", + "defaultValue": "20" + }, + { + "id": "param__recovery_response", + "name": "Recovery response time (hours)", + "variableName": "recovery_response", + "type": "real", + "defaultValue": "2.5" + }, + { + "id": "param__tow_time", + "name": "Tow time (hours)", + "variableName": "tow_time", + "type": "real", + "defaultValue": "3" + }, + { + "id": "param__repair_time", + "name": "Repair after a breakdown (hours)", + "variableName": "repair_time", + "type": "real", + "defaultValue": "12" + }, + { + "id": "param__fuel_per_km", + "name": "Base fuel consumption (litres per km)", + "variableName": "fuel_per_km", + "type": "real", + "defaultValue": "0.35" + }, + { + "id": "param__max_driving_hours", + "name": "Maximum driving hours before rest", + "variableName": "max_driving_hours", + "type": "real", + "defaultValue": "9" + }, + { + "id": "param__conditions_step", + "name": "Per-truck diffusion step (hours)", + "variableName": "conditions_step", + "type": "real", + "defaultValue": "0.5" + }, + { + "id": "param__severity_reversion", + "name": "Road severity OU reversion rate", + "variableName": "severity_reversion", + "type": "real", + "defaultValue": "0.8" + }, + { + "id": "param__severity_volatility", + "name": "Road severity OU volatility", + "variableName": "severity_volatility", + "type": "real", + "defaultValue": "0.15" + }, + { + "id": "param__speed_reversion", + "name": "Speed factor OU reversion rate", + "variableName": "speed_reversion", + "type": "real", + "defaultValue": "0.6" + }, + { + "id": "param__speed_volatility", + "name": "Speed factor OU volatility", + "variableName": "speed_volatility", + "type": "real", + "defaultValue": "0.08" + }, + { + "id": "param__env_step", + "name": "Global environment shift interval (hours)", + "variableName": "env_step", + "type": "real", + "defaultValue": "4" + }, + { + "id": "param__env_reversion", + "name": "Global environment OU reversion rate", + "variableName": "env_reversion", + "type": "real", + "defaultValue": "0.1" + }, + { + "id": "param__env_volatility", + "name": "Global environment OU volatility", + "variableName": "env_volatility", + "type": "real", + "defaultValue": "0.05" + }, + { + "id": "param__base_severity_mean", + "name": "Long-run severity mean", + "variableName": "base_severity_mean", + "type": "real", + "defaultValue": "1.0" + }, + { + "id": "param__base_speed_mean", + "name": "Long-run speed mean", + "variableName": "base_speed_mean", + "type": "real", + "defaultValue": "1.0" + }, + { + "id": "param__fuel_cost_per_unit", + "name": "Fuel cost per litre", + "variableName": "fuel_cost_per_unit", + "type": "real", + "defaultValue": "1.5" + }, + { + "id": "param__repair_cost", + "name": "Cost per breakdown repair", + "variableName": "repair_cost", + "type": "real", + "defaultValue": "3500" + }, + { + "id": "param__service_cost", + "name": "Cost per planned service", + "variableName": "service_cost", + "type": "real", + "defaultValue": "800" + }, + { + "id": "param__rest_penalty", + "name": "Penalty per mandatory rest event", + "variableName": "rest_penalty", + "type": "real", + "defaultValue": "150" + }, + { + "id": "param__late_penalty_fraction", + "name": "Revenue lost on late delivery", + "variableName": "late_penalty_fraction", + "type": "real", + "defaultValue": "0.3" + }, + { + "id": "param__wear_feedback", + "name": "How much wear already carried accelerates further wear", + "variableName": "wear_feedback", + "type": "real", + "defaultValue": "0.6" + } + ], + "scenarios": [ + { + "id": "scenario__run_to_failure", + "name": "Run to failure", + "description": "Nothing is serviced on condition: trucks are only ever repaired after they fail.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 9 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__condition_based", + "name": "Condition-based servicing", + "description": "Trucks come in when any component's wear reaches the threshold. Everything else is identical to the baseline.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__early_servicing", + "name": "Servicing too early", + "description": "The same rule at a quarter of full wear. Fewer breakdowns, but the fleet spends its life in the workshop.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.25 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__route_restriction", + "name": "Condition-based, worn trucks off mountain work", + "description": "Condition-based servicing plus a dispatch rule: a truck past a third of full wear is not sent on mountain routes.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "severe_route_wear_limit", + "default": 0.35 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__severe_route_wear_limit": "scenario.severe_route_wear_limit" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__single_bay", + "name": "Condition-based, one bay", + "description": "Condition-based servicing with the second bay closed. Planned services and breakdown repairs compete for one bay.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 1 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__thin_spares", + "name": "Condition-based, one part on the shelf", + "description": "Condition-based servicing with a single spare and a long parts lead time.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 1 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "parts_lead_time", + "default": 72 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__parts_lead_time": "scenario.parts_lead_time" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__winter", + "name": "Winter conditions", + "description": "Icy roads, slower speeds, more wear. Severity mean 1.4, speed mean 0.8.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "base_severity_mean", + "default": 1.4 + }, + { + "type": "real", + "identifier": "base_speed_mean", + "default": 0.8 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__base_severity_mean": "scenario.base_severity_mean", + "param__base_speed_mean": "scenario.base_speed_mean" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__summer", + "name": "Summer baseline", + "description": "Dry roads, faster speeds. Severity mean 0.9, speed mean 1.1.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "base_severity_mean", + "default": 0.9 + }, + { + "type": "real", + "identifier": "base_speed_mean", + "default": 1.1 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__base_severity_mean": "scenario.base_severity_mean", + "param__base_speed_mean": "scenario.base_speed_mean" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__demand_surge", + "name": "Demand surge", + "description": "Load arrival rates multiplied by 1.5.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "motorway_rate", + "default": 0.105 + }, + { + "type": "real", + "identifier": "urban_rate", + "default": 0.1425 + }, + { + "type": "real", + "identifier": "mountain_rate", + "default": 0.072 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__motorway_rate": "scenario.motorway_rate", + "param__urban_rate": "scenario.urban_rate", + "param__mountain_rate": "scenario.mountain_rate" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__winter_surge", + "name": "Winter + demand surge", + "description": "The compound scenario: bad weather and high demand together.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "base_severity_mean", + "default": 1.4 + }, + { + "type": "real", + "identifier": "base_speed_mean", + "default": 0.8 + }, + { + "type": "real", + "identifier": "motorway_rate", + "default": 0.105 + }, + { + "type": "real", + "identifier": "urban_rate", + "default": 0.1425 + }, + { + "type": "real", + "identifier": "mountain_rate", + "default": 0.072 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__base_severity_mean": "scenario.base_severity_mean", + "param__base_speed_mean": "scenario.base_speed_mean", + "param__motorway_rate": "scenario.motorway_rate", + "param__urban_rate": "scenario.urban_rate", + "param__mountain_rate": "scenario.mountain_rate" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + }, + { + "id": "scenario__route_aware_winter", + "name": "Route-aware dispatch + winter", + "description": "Tests whether the route restriction rule becomes more valuable in bad weather. Mountain wear limit 0.35, winter conditions.", + "scenarioParameters": [ + { + "type": "integer", + "identifier": "trucks", + "default": 8 + }, + { + "type": "integer", + "identifier": "drivers", + "default": 8 + }, + { + "type": "integer", + "identifier": "bays", + "default": 2 + }, + { + "type": "integer", + "identifier": "technicians", + "default": 2 + }, + { + "type": "integer", + "identifier": "spares", + "default": 10 + }, + { + "type": "integer", + "identifier": "recovery_units", + "default": 2 + }, + { + "type": "real", + "identifier": "service_wear_limit", + "default": 0.5 + }, + { + "type": "real", + "identifier": "severe_route_wear_limit", + "default": 0.35 + }, + { + "type": "real", + "identifier": "base_severity_mean", + "default": 1.4 + }, + { + "type": "real", + "identifier": "base_speed_mean", + "default": 0.8 + } + ], + "parameterOverrides": { + "param__service_wear_limit": "scenario.service_wear_limit", + "param__severe_route_wear_limit": "scenario.severe_route_wear_limit", + "param__base_severity_mean": "scenario.base_severity_mean", + "param__base_speed_mean": "scenario.base_speed_mean" + }, + "initialState": { + "type": "code", + "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n FuelSpent: 0,\n };" + } + } + ], + "metrics": [ + { + "id": "metric__loads_delivered", + "name": "Loads delivered on time", + "description": "Loads that reached the customer inside the window.", + "code": "return state.places.DeliveredLoads.count;" + }, + { + "id": "metric__loads_late", + "name": "Loads delivered late", + "description": "Loads that arrived outside the window.", + "code": "return state.places.LateLoads.count;" + }, + { + "id": "metric__loads_dropped", + "name": "Loads dropped", + "description": "Loads nobody collected plus loads lost to a breakdown.", + "code": "return state.places.DroppedLoads.count;" + }, + { + "id": "metric__service_level", + "name": "Service level", + "description": "Share of offered loads delivered on time.", + "code": "const delivered = state.places.DeliveredLoads.count;\nconst late = state.places.LateLoads.count;\nconst dropped = state.places.DroppedLoads.count;\nconst offered = delivered + late + dropped;\nreturn offered === 0 ? 1 : delivered / offered;" + }, + { + "id": "metric__revenue", + "name": "Revenue", + "description": "Revenue from delivered loads. Late loads are paid at a discount.", + "code": "const onTime = state.places.DeliveredLoads.tokens.reduce(\n (total, load) => total + load.revenue, 0);\nconst late = state.places.LateLoads.tokens.reduce(\n (total, load) => total + load.revenue * (1 - parameters.late_penalty_fraction), 0);\nreturn onTime + late;" + }, + { + "id": "metric__total_fuel", + "name": "Total fuel burned", + "description": "Litres consumed across the fleet.", + "code": "const fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nreturn fleet.reduce((total, truck) => total + truck.fuel_burned, 0);" + }, + { + "id": "metric__operating_cost", + "name": "Operating cost", + "description": "Fuel cost plus repairs and services.", + "code": "const fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nconst fuel = fleet.reduce((total, truck) => total + truck.fuel_burned, 0) * parameters.fuel_cost_per_unit;\nconst repairs = state.places.RepairsDone.count * parameters.repair_cost;\nconst services = state.places.ServicesDone.count * parameters.service_cost;\nconst rest = state.places.RestEvents.count * parameters.rest_penalty;\nreturn fuel + repairs + services + rest;" + }, + { + "id": "metric__profit", + "name": "Profit", + "description": "Revenue minus operating cost minus late delivery penalties.", + "code": "const onTime = state.places.DeliveredLoads.tokens.reduce(\n (total, load) => total + load.revenue, 0);\nconst late = state.places.LateLoads.tokens.reduce(\n (total, load) => total + load.revenue * (1 - parameters.late_penalty_fraction), 0);\nconst revenue = onTime + late;\nconst fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nconst fuel = fleet.reduce((total, truck) => total + truck.fuel_burned, 0) * parameters.fuel_cost_per_unit;\nconst repairs = state.places.RepairsDone.count * parameters.repair_cost;\nconst services = state.places.ServicesDone.count * parameters.service_cost;\nconst rest = state.places.RestEvents.count * parameters.rest_penalty;\nreturn revenue - fuel - repairs - services - rest;" + }, + { + "id": "metric__roadside_failures", + "name": "Roadside failures", + "description": "Breakdowns away from the depot.", + "code": "return state.places.RoadsideEvents.count;" + }, + { + "id": "metric__services", + "name": "Planned services", + "description": "Trucks brought in on the wear rule and returned to as-new condition.", + "code": "return state.places.ServicesDone.count;" + }, + { + "id": "metric__repairs", + "name": "Unplanned repairs", + "description": "Trucks repaired after a breakdown.", + "code": "return state.places.RepairsDone.count;" + }, + { + "id": "metric__deferred_services", + "name": "Services deferred", + "description": "Times a truck was due for a service, found the workshop full.", + "code": "return state.places.DeferredServices.count;" + }, + { + "id": "metric__rest_events", + "name": "Driver rest events", + "description": "Times a truck was sent to mandatory rest.", + "code": "return state.places.RestEvents.count;" + }, + { + "id": "metric__trucks_earning", + "name": "Trucks earning", + "description": "Trucks on a route right now.", + "code": "return state.places.OnRoute.count;" + }, + { + "id": "metric__trucks_off_road", + "name": "Trucks off the road", + "description": "Trucks in a bay, waiting for a part, under recovery, or resting.", + "code": "return state.places.InBay.count + state.places.AwaitingParts.count + state.places.Stranded.count + state.places.UnderRecovery.count + state.places.Rest.count;" + }, + { + "id": "metric__fleet_utilisation", + "name": "Fleet utilisation", + "description": "Fraction of trucks on route or returning vs the total fleet.", + "code": "const fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nif (fleet.length === 0) return 0;\nconst earning = state.places.OnRoute.count + state.places.Returning.count;\nreturn earning / fleet.length;" + }, + { + "id": "metric__avg_brake_wear", + "name": "Average brake wear", + "description": "Mean brake wear across the fleet.", + "code": "const fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nif (fleet.length === 0) return 0;\nreturn fleet.reduce((t, tr) => t + tr.brake_wear, 0) / fleet.length;" + }, + { + "id": "metric__avg_engine_wear", + "name": "Average engine wear", + "description": "Mean engine wear across the fleet.", + "code": "const fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nif (fleet.length === 0) return 0;\nreturn fleet.reduce((t, tr) => t + tr.engine_wear, 0) / fleet.length;" + }, + { + "id": "metric__avg_tyre_wear", + "name": "Average tyre wear", + "description": "Mean tyre wear across the fleet.", + "code": "const fleet = state.places.Available.tokens.concat(state.places.OnRoute.tokens.concat(state.places.Returning.tokens.concat(state.places.DepotQueue.tokens.concat(state.places.InBay.tokens.concat(state.places.AwaitingParts.tokens.concat(state.places.Stranded.tokens.concat(state.places.UnderRecovery.tokens.concat(state.places.Rest.tokens))))))));\nif (fleet.length === 0) return 0;\nreturn fleet.reduce((t, tr) => t + tr.tyre_wear, 0) / fleet.length;" + } + ], + "subnets": [], + "componentInstances": [], + "version": 1, + "meta": { + "generator": "Petrinaut" + }, + "title": "Truck fleet with condition-based maintenance (v2)" +} diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md b/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md deleted file mode 100644 index 54e343fb836..00000000000 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md +++ /dev/null @@ -1,122 +0,0 @@ -# Project coordination - -This is the cross-map coordination surface for the `brunch-agent` project. FE-1383 owns the -milestone-one harness build; FE-1357 owns the September demo and process-model plugin design. -Linear is canonical for issue state, parentage, and hard `blocks` relations. This file owns -only the judgment Linear cannot express: the current project-wide recommendation, soft edges, -unresolved seams, and exceptional roots. - -Before revising the recommendation, run -`turbo run linear:graph --filter '@hashintel/brunch-agent'`. Its compact projection supplies the -factual open-issue DAG; read the relevant issue bodies for semantic content, then infer the smallest -honest recommendation. Do not paste the generated graph here or mirror issue status. - -## Current sequencing recommendation - -As of **2026-08-21**, the FE-1437 authority cutover has been executed: the full brunch-lite -history is imported on `ln/fe-1437-hash-monorepo-import` in `hashintel/hash` (frozen standalone -SHA `43a0022918861846344b96a32cb94f92e2ee96ae`), every import gate re-verified. `hashintel/hash` -is authoritative; the standalone repository accepts no further implementation work. FE-1437 -closes when the branch lands on `main` (squash merge, per convention). Part of FE-1440's website -wiring (the Brunch interactive-tool panel in `apps/petrinaut-website`) travelled with the import -branch; FE-1440 was trimmed on 2026-08-21 to the remaining mode wiring (mode switch, browser -identifier bootstrap, remote transport swap). After landing, advance FE-1438 (client-tool round-trip) -beside FE-1393 (plugin SDK and first projection); FE-1439 (private durable sessions) proceeds in -parallel. The integration stream joins at FE-1440 and deployment follows at FE-1441 (which also -waits on FE-1423's pre-exposure gates), while the harness stream reaches its contract-freeze -decision at FE-1387. FE-1402/FE-1403 form a parallel content/evaluation stream, without -displacing the two convergence edges. - -```text -legend: - -[hard]-> native Linear blocker - -[coord]-> either order; do not implement concurrently - -[input]-> semantic input, not a blocker - -[state-gate]-> condition in the world, not an issue edge - -nodes: - FE-1437 [executed, landing] # history imported; HASH authoritative; PR pending - FE-1438 [next] # client-tool round-trip - FE-1439 [next, parallel] # private durable sessions - FE-1440 [join, partly landed] # website elicitor mode; panel wiring on import branch - FE-1441 [post-landing] # HASH deployment - FE-1393 [next] # plugin SDK + first projection - FE-1387 [after-FE-1393] # second pack + contract freeze - FE-1395 [coordination] # full affordance set - FE-1402 [parallel, content] # completion contract - FE-1403 [parallel, content] # interviewing guidance - FE-1404 [after-content] # armed baseline - -edges: - FE-1449 -[hard]-> FE-1438 - FE-1392 -[hard]-> FE-1393 - FE-1438, FE-1439, - FE-1437 -[hard]-> FE-1440 - FE-1437, FE-1439, - FE-1423 -[hard]-> FE-1441 - FE-1393 -[hard]-> FE-1387 - FE-1402, FE-1403 -[hard]-> FE-1404 - FE-1395 -[coord]-> FE-1438 - FE-1437 branch on main -[state-gate]-> FE-1438, FE-1439, FE-1393 - FE-1387 -[input]-> FE-1440 -``` - -Hard-edge truth remains in Linear. The graph above is a deliberately focused recommendation, -not a second issue database. - -## Repository handoff threshold - -FE-1437 (the monorepo import; [execution plan](./hash-monorepo-import-plan.md)) was the authority -cutover, not a general freeze on harness work. It was crossed on 2026-08-21: - -```text -brunch-lite authoritative (until 2026-08-21) - FE-1434 + FE-1435 verdicts landed - FE-1388/1389/1390/1399 review stack merged - | - v - == FE-1437 import (executed) == - | - v -hashintel/hash authoritative (now) - FE-1440 website wiring + FE-1441 deployment -``` - -The standalone repository is frozen at SHA `43a0022918861846344b96a32cb94f92e2ee96ae` and is -read-only reference material. All further work β€” including FE-1438 and FE-1439, which were not -import gates β€” happens in `hashintel/hash`. Do not run both repositories as writable authorities. -Closing out the standalone repository's shared state (archival, access) is deferred and requires -explicit approval from Lu. - -## Open seams - -- **Contract freeze β€” FE-1387.** The process-model target must stress the plugin contract before - it freezes. FE-1393 makes the contract programmable; FE-1387 remains the cross-map hinge. -- **Absence locator.** An absence capture carries no payload, but the fold needs a field-specific - coordinate (anchor Γ— slot). The plugin-contract spec records three worked cases; any envelope - amendment belongs to the harness side of this seam. -- **Structured-tap evidence β€” FE-1395 Γ— capture store.** `resolve-conflict` currently rejects - `user-affordance-payload` evidence. FE-1395 must decide the transport fact before the store - rule can settle. -- **Guidance placement β€” FE-1403 Γ— FE-1406.** Plugin cards and the harness-shipped generic - strategy quiver share authoring methods but not ownership. Packages export; hosts register. -- **Telemetry vocabulary β€” FE-1385 Γ— FE-1404 Γ— FE-1423.** The probe surface, experiment - accounting, and remote telemetry should share span vocabulary rather than invent it three - times. -- **Living-prototype charter.** The deployed elicitor as a cumulative record of proved and - unproved behavior remains pre-charter until the infrastructure conversation settles. - -Settled seam decisions still governing open work: FE-1392 established the plugin-declared -verbatim proposal floor, and ask accounting remains a read-time relation rather than an envelope -field. ADR-0003 keeps field-level structure below the capture's single epistemic status. - -## Exceptional roots - -These project issues intentionally have no parent and are roots under the registry rule: - -- **FE-1331 β€” start elicitation from Petrinaut's create-new-net flow.** Deferred post-September - consumer topology; returns after in-Petrinaut staging proves itself. -- **FE-1334 β€” offer the user a surprising scenario of their model.** A validation gesture with - no owning map yet; closest to the motif/quiver strategy work. -- **FE-1406 β€” design reusable elicitation strategies.** The cross-map home for the - harness-shipped generic strategy quiver; intentionally independent of either delivery map. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/capture-store-plain.md b/libs/@hashintel/brunch-agent/docs/reference/architecture/capture-store.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/capture-store-plain.md rename to libs/@hashintel/brunch-agent/docs/reference/architecture/capture-store.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/flue-architecture-cheatsheet.md b/libs/@hashintel/brunch-agent/docs/reference/architecture/flue-architecture-cheatsheet.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/flue-architecture-cheatsheet.md rename to libs/@hashintel/brunch-agent/docs/reference/architecture/flue-architecture-cheatsheet.md index 0147008a202..e810602c620 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/flue-architecture-cheatsheet.md +++ b/libs/@hashintel/brunch-agent/docs/reference/architecture/flue-architecture-cheatsheet.md @@ -5,12 +5,12 @@ An architect's consolidation of the Flue documentation β€” all 21 guide entrypoi FE-1383/FE-1357 roadmap. Purpose (Lu's charter): align with recommended patterns and use provided affordances _before_ we diverge by building layers we don't need or drawing boundaries in the wrong place. Companion to the narrower usage audit -([`flue-patterns-audit-2026-08-17.md`](../legibility-sweep/flue-patterns-audit-2026-08-17.md)); same caveat β€” +([`flue-patterns-audit-2026-08-17.md`](../../evidence/proofs/audits/flue-patterns-audit-2026-08-17.md)); same caveat β€” WebFetch summarizes, so unquoted API details are paraphrase-grade. The FE-1391 B1/B2 gate later checked the installed 2.0.3 source, exported types, and package-shipped docs directly. Its corrections below are source-grade and link to the -[source-read record](../legibility-sweep/flue-entry-projection-source-read-2026-08-18.md). +[source-read record](../../evidence/proofs/audits/flue-entry-projection-source-read-2026-08-18.md). Link form: every page has a markdown mirror at `/index.md`. diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/topology.md b/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md similarity index 97% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/topology.md rename to libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md index 3d4351c5411..1bfcb660c8d 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/topology.md +++ b/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md @@ -71,7 +71,7 @@ apps/brunch-agent LANE 1 SHELL + remote server (imported from a └─ test/ βœ“ reviewed substrate inventory; child-process eval (audited: composed from documented parts; do-not-weaken pins live here) -docs/planning/**/baseline/run.ts EXPERIMENT (JS-API workflow pattern, independently converged) +evaluations/protocols/process-model-elicitation/baseline/run.ts EXPERIMENT PROTOCOL ``` ## Specification β€” where what's next lands @@ -97,7 +97,7 @@ docs/planning/**/baseline/run.ts EXPERIMENT (JS-API workflow pattern, independ Applications may compose Brunch and Petrinaut public surfaces; reusable libraries may not know about one another. - **N4 (experiments).** Experiment runners (FE-1404, future condition reruns) stay beside their - planning docs (`docs/planning/**/`), JS-API pattern, `observe()` accounting β€” never in + the JS-API pattern with `observe()` accounting β€” never in `packages/` (they are instruments, not product) and never a bespoke daemon. - **N5 (storage-port implementations; local target discharged by FE-1391).** One per (binding Γ— deploy target), always in the binding package, always implementing core's `CaptureStore` + diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/elicitation-strategy-literature.md b/libs/@hashintel/brunch-agent/docs/reference/research/elicitation/elicitation-strategy-literature.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/elicitation-strategy-literature.md rename to libs/@hashintel/brunch-agent/docs/reference/research/elicitation/elicitation-strategy-literature.md index dd4d2873e69..eb094a353cd 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/elicitation-strategy-literature.md +++ b/libs/@hashintel/brunch-agent/docs/reference/research/elicitation/elicitation-strategy-literature.md @@ -1,9 +1,9 @@ # Elicitation strategy: what the literature already knows Resolves the research half of FE-1360. Companion to -[open-questions-elicitation-design-2026-08-11](../notes/open-questions-elicitation-design-2026-08-11.md) +[open-questions-elicitation-design-2026-08-11](../../../archive/external-snapshots/open-questions-elicitation-design-2026-08-11.md) (the question inventory) and -[expert-meeting-findings-2026-08-11](../notes/expert-meeting-findings-2026-08-11.md) +[expert-meeting-findings-2026-08-11](../../../evidence/proofs/research/expert-meeting-findings-2026-08-11.md) (which established that there is no in-house interviewing practice to elicit from, activating the literature fallback). diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/re-interviewing-literature-worker-report.md b/libs/@hashintel/brunch-agent/docs/reference/research/elicitation/interviewing-literature-source-catalog.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/re-interviewing-literature-worker-report.md rename to libs/@hashintel/brunch-agent/docs/reference/research/elicitation/interviewing-literature-source-catalog.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/petrinaut-survey.md b/libs/@hashintel/brunch-agent/docs/reference/research/petrinaut-survey.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/petrinaut-survey.md rename to libs/@hashintel/brunch-agent/docs/reference/research/petrinaut-survey.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/voice-feasibility.md b/libs/@hashintel/brunch-agent/docs/reference/research/voice-feasibility.md similarity index 98% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/voice-feasibility.md rename to libs/@hashintel/brunch-agent/docs/reference/research/voice-feasibility.md index ddf3f7cfd9a..efdb853802e 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/research/voice-feasibility.md +++ b/libs/@hashintel/brunch-agent/docs/reference/research/voice-feasibility.md @@ -1,8 +1,8 @@ # Voice-first elicitation: feasibility against the elicitation kernel Resolves FE-1359. Written 2026-08-11 against -[`spec.md`](../../../spec.md) (draft assembled 2026-08-10, reviewed twice since), -the inbox note [voice-implementation-recommendation-pplx](../../../reference/voice-implementation-recommendation-pplx.md), +[`spec.md`](../../specs/elicitation-kernel.md) (draft assembled 2026-08-10, reviewed twice since), +the inbox note [voice-implementation-recommendation-pplx](../voice-implementation-recommendation-pplx.md), the three prototype branches (`prototype/10-flue-roundtrip`, `prototype/11-capture-sweep`, `prototype/13-sweep-seam`), and web verification of the provider landscape (sources at the end). @@ -613,22 +613,22 @@ the kernel loop is demonstrably done with two weeks to spare. Spec and repo (primary): -- [`spec.md`](../../../spec.md) Β§4, Β§5, Β§5.1, Β§7.2–§7.7, Β§8.1–§8.3, Β§9.1–§9.6, Β§10, +- [`spec.md`](../../specs/elicitation-kernel.md) Β§4, Β§5, Β§5.1, Β§7.2–§7.7, Β§8.1–§8.3, Β§9.1–§9.6, Β§10, Β§11.4, Β§12.6, Β§13.1 -- [`CONTEXT.md`](../../../../CONTEXT.md) β€” ui-shell definition -- [`issues/01-flue-architecture-deep-read.md`](../../../history/planning/elicitation-kernel/issues/01-flue-architecture-deep-read.md) +- [`CONTEXT.md`](../../../CONTEXT.md) β€” ui-shell definition +- [`issues/01-flue-architecture-deep-read.md`](../../archive/elicitation-kernel/issues/01-flue-architecture-deep-read.md) β€” durable-submission terminal outcomes and abort signal; `useDataWriter` one-way; `useAgentStart` load-data seam; `@flue/sdk` for non-React hosts -- [`issues/10-walking-skeleton-flue-roundtrip.md`](../../../history/planning/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md) +- [`issues/10-walking-skeleton-flue-roundtrip.md`](../../archive/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md) β€” turn suspension proven, wake wart, update-in-place data channel - `prototypes/sweep-seam/src/ui/chat.tsx` on `prototype/13-sweep-seam` β€” the existing reply transport (`useFlueAgent`, `agent.sendMessage(text)`), tap-as-string, `StorePanel` -- [`docs/reference/yannis-dora-lu-transcript-2026-08-11.md`](../../../reference/yannis-dora-lu-transcript-2026-08-11.md) +- [`docs/reference/yannis-dora-lu-transcript-2026-08-11.md`](../yannis-dora-lu-transcript-2026-08-11.md) β€” the ASR-fidelity evidence Provider and practice (web, fetched 2026-08-11): -- [`docs/reference/voice-implementation-recommendation-pplx.md`](../../../reference/voice-implementation-recommendation-pplx.md) +- [`docs/reference/voice-implementation-recommendation-pplx.md`](../voice-implementation-recommendation-pplx.md) β€” the prior recommendation this document refines and partly dissents from - [ElevenLabs Speech Engine overview](https://elevenlabs.io/docs/overview/capabilities/speech-engine) β€” "adds voice capabilities to any chat agent"; "your server provides the LLM logic"; SDK "manages diff --git a/libs/@hashintel/brunch-agent/docs/spec.md b/libs/@hashintel/brunch-agent/docs/specs/elicitation-kernel.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/spec.md rename to libs/@hashintel/brunch-agent/docs/specs/elicitation-kernel.md index 26d06ccffec..1fad7129adb 100644 --- a/libs/@hashintel/brunch-agent/docs/spec.md +++ b/libs/@hashintel/brunch-agent/docs/specs/elicitation-kernel.md @@ -2,10 +2,10 @@ Status: draft for review Assembled: 2026-08-10, from the resolved -[wayfinder map](history/planning/elicitation-kernel/map.md) (tickets 01–13), the two inbox references -([challenges](reference/agentic-elicitation-challenges-2026-08-06T10-02-41Z.md), -[criteria](reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md)), and the -[2026-08-10 consistency pre-pass](history/planning/elicitation-kernel/notes/consistency-prepass-2026-08-10.md). +[wayfinder map](../archive/elicitation-kernel/map.md) (tickets 01–13), the two inbox references +([challenges](../reference/agentic-elicitation-challenges-2026-08-06T10-02-41Z.md), +[criteria](../reference/agentic-elicitation-criteria-2026-08-06T14-11-18Z.md)), and the +[2026-08-10 consistency pre-pass](../archive/elicitation-kernel/notes/consistency-prepass-2026-08-10.md). Contradiction adjudications are collected in [Appendix A](#appendix-a--adjudications). "Elicitation kernel" and "brunch-lite" are working labels; the real product name is unresolved @@ -48,7 +48,7 @@ code. The system is fully decoupled from brunch's September MVP. ## 3. Vocabulary -The canonical glossary is context [`CONTEXT.md`](../CONTEXT.md) β€” shells (substrate / ui / harness +The canonical glossary is context [`CONTEXT.md`](../../CONTEXT.md) β€” shells (substrate / ui / harness / plugin / binding), sessions and durability (target-domain / target-document / session / capture store / re-entry briefing), and interaction terms (affordance / capture / sweep / settlement / interpretation render), now extended with the envelope vocabulary this spec relies on (capture diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-design-plain.md b/libs/@hashintel/brunch-agent/docs/specs/intermediate-representation-plain.md similarity index 97% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-design-plain.md rename to libs/@hashintel/brunch-agent/docs/specs/intermediate-representation-plain.md index 270d5132485..f099ad726d9 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-design-plain.md +++ b/libs/@hashintel/brunch-agent/docs/specs/intermediate-representation-plain.md @@ -1,6 +1,10 @@ # The intermediate representation, in plain language -> This is a plain-prose rendering of [`ir-design.md`](./ir-design.md), written for review reading. It makes the same content commitments as the original, which remains the authoritative document. The rendering pass doubled as a review instrument: seven places where the source resisted plain rendering are recorded as findings on FE-1401 (third accrual), the load-bearing one being the loss report's unresolved unit of loss (capture vs. capture-facet). +> This is a non-authoritative plain-prose legibility companion to the authoritative +> [`intermediate-representation.md`](intermediate-representation.md). The rendering pass doubled as +> a review instrument: seven places where the source resisted plain rendering are recorded as +> findings on FE-1401 (third accrual), the load-bearing one being the loss report's unresolved unit +> of loss (capture vs. capture-facet). This design was resolved on 13 August 2026 and ratified on worked examples by FE-1397. It has two layers. Layer A defines what an intermediate representation (IR) is for any plugin. Layer B is the concrete design for the CPS plugin β€” the design the September demo will run on. The design draws on the kernel spec, the Petrinaut survey's format facts, Dora's PRO-98 ontology, the open-questions document, and the FE-1363 use-case resolution. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-design.md b/libs/@hashintel/brunch-agent/docs/specs/intermediate-representation.md similarity index 98% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-design.md rename to libs/@hashintel/brunch-agent/docs/specs/intermediate-representation.md index 2ad5c654839..6bf802cc848 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/ir-design.md +++ b/libs/@hashintel/brunch-agent/docs/specs/intermediate-representation.md @@ -6,11 +6,11 @@ CPS plugin's specific payload design, the September working design. Inputs: the (Β§5, Β§6, Β§11), the Petrinaut survey's format facts, Dora's PRO-98 ontology (its Maps-to column), the open-questions doc Β§7, and the FE-1363 use-case resolution. Layer-A amendments from the worked-examples exercise are marked _(amended FE-1397)_; the exercise itself is -[`ir-worked-examples.md`](./ir-worked-examples.md). +[`ir-worked-examples.md`](../evidence/proofs/design/intermediate-representation-worked-examples.md). ## Layer A β€” what "the IR" is, architecturally -**Definition.** _(amended by [ADR-0003](../../adr/0003-three-register-ir.md), 2026-08-18 β€” +**Definition.** _(amended by [ADR-0003](../adr/0003-three-register-ir.md), 2026-08-18 β€” the IR proper is the elicited conceptual model, derived by a pure fold over active captures; the sentence below describes register 1 of three, and "no second store" survives because register 2 is a derivation, never a persistence surface.)_ The intermediate representation of @@ -71,7 +71,7 @@ opaque plugin payload. Defining an IR is therefore defining a payload type syste **Status: ratified on worked examples** (Lu, 2026-08-13, FE-1397). The ratification condition β€” speculative payload designs across at least three plugin targets at different complexity levels, checked property by property β€” is discharged in -[`ir-worked-examples.md`](./ir-worked-examples.md): Gherkin (thin), CPS (thick, this document's +[`ir-worked-examples.md`](../evidence/proofs/design/intermediate-representation-worked-examples.md): Gherkin (thin), CPS (thick, this document's Layer B), BPMN/process-mining (mid), with the assurance plugin (spec Β§13.2) as a fourth free corroborant. All five MUST properties survive β€” 2 and 3 amended as worded above β€” and the expected sublimation pressure β€” a payload-level concept proving so universal it rises out of diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/petrinaut-integration-spec.md b/libs/@hashintel/brunch-agent/docs/specs/petrinaut-integration.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/petrinaut-integration-spec.md rename to libs/@hashintel/brunch-agent/docs/specs/petrinaut-integration.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/plugin-contract-spec.md b/libs/@hashintel/brunch-agent/docs/specs/plugin-contract.md similarity index 98% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/plugin-contract-spec.md rename to libs/@hashintel/brunch-agent/docs/specs/plugin-contract.md index dc941454807..47b587737ca 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/plugin-contract-spec.md +++ b/libs/@hashintel/brunch-agent/docs/specs/plugin-contract.md @@ -1,7 +1,7 @@ # Spec: the plugin contract β€” two schemas, two tables Status: **provisional** β€” desk-validated on baseline transcripts and cross-plugin thumbnails. -Ratification condition (inherited from [ADR-0003](../../adr/0003-three-register-ir.md)): a full +Ratification condition (inherited from [ADR-0003](../adr/0003-three-register-ir.md)): a full FE-1397-style worked pass across at least three plugin targets. Until the September build exercises a real fold, everything here is design, not demonstrated behavior. Decided on: FE-1405 (the payload-interiors session, 2026-08-18); inputs were that session's @@ -363,7 +363,8 @@ decidedness. worked-instance gold set as the accuracy rubric (FE-1407). - **Envelope pressure #2 β€” absence locator (seam, unresolved).** Absence captures carry no payload; a field-specific absence cannot name its slot. Three C1 cases logged. The needed - locator is a fold-table coordinate. Owner: the FE-1383 seam (`COORDINATION.md`), for + locator is a fold-table coordinate. Current authority: the active soft edge in + [STEERING](../control/STEERING.md#active-soft-edges), for adjudication β€” the register-2 `absent` state is representable but not populatable until it lands. @@ -386,7 +387,7 @@ decidedness. - This document is the settled form of the FE-1405 session's working draft and its ds-pseudo YAML rendering β€” untracked session artifacts (`drafts/`, per the documentation protocol) that collapsed into this spec and are not load-bearing anywhere. -- The decision this spec builds on is [ADR-0003](../../adr/0003-three-register-ir.md); the +- The decision this spec builds on is [ADR-0003](../adr/0003-three-register-ir.md); the worked cross-plugin thumbnails (Gherkin thin, formal verification mid, CPS thick) live in the working draft and discharge the ratification bar only provisionally. - The authoring story is the product claim to protect: _declare your kinds and slots; declare diff --git a/libs/@hashintel/brunch-agent/evaluations/AGENTS.md b/libs/@hashintel/brunch-agent/evaluations/AGENTS.md new file mode 100644 index 00000000000..a5aedfc307d --- /dev/null +++ b/libs/@hashintel/brunch-agent/evaluations/AGENTS.md @@ -0,0 +1,8 @@ +# Evaluation assets + +- `cases/` owns reusable domain/source truth and interviewee-visible inputs. +- `protocols/` owns prompts, runners, and procedures. +- `oracles/` owns reviewed expected claims and answer keys; keep them outside interviewee and model inputs. +- Generated or observed run evidence belongs under `docs/evidence/evaluations/`, not here. +- Preserve provenance; never silently overwrite immutable snapshots. +- Tests must use test-only output paths. diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/opening-message.md b/libs/@hashintel/brunch-agent/evaluations/cases/process-model-elicitation/baseline/opening-message.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/opening-message.md rename to libs/@hashintel/brunch-agent/evaluations/cases/process-model-elicitation/baseline/opening-message.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/situation-pack.md b/libs/@hashintel/brunch-agent/evaluations/cases/process-model-elicitation/baseline/situation-pack.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/situation-pack.md rename to libs/@hashintel/brunch-agent/evaluations/cases/process-model-elicitation/baseline/situation-pack.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/protocol.md b/libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/protocol.md similarity index 87% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/protocol.md rename to libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/protocol.md index d4166947abd..592461460ef 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/protocol.md +++ b/libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/protocol.md @@ -2,7 +2,8 @@ What does one-shot / lightly-prompted AI elicitation already achieve? Two conditions of the three-condition design (condition 3, kernel harness + real plugin, is out of this ticket's -scope). The read-out lives in [readout.md](readout.md). +scope). The read-out lives in the immutable +[evaluation evidence](../../../../docs/evidence/evaluations/process-model-elicitation/baseline/readout.md). ## Conditions @@ -12,7 +13,8 @@ scope). The read-out lives in [readout.md](readout.md). | 2 | `claude-opus-5` | [v0-prompt.md](v0-prompt.md) | the degenerate plugin: the seven-category elicitation surface as pure guidance, no machinery | Both conditions receive the identical opening user message -([opening-message.md](opening-message.md)); the v0 system prompt is the only difference, so +([opening-message.md](../../../cases/process-model-elicitation/baseline/opening-message.md)); the +v0 system prompt is the only difference, so the 1β†’2 delta measures what pack content alone buys. ## Subject and interviewee @@ -20,7 +22,9 @@ the 1β†’2 delta measures what pack content alone buys. Subject: **Production Process Scheduling** (Notion use-case DB, the mature spec'd case with a reference model β€” FE-1363 retained it as the flat-baseline testbed). The interviewee is a simulated expert per the FE-1363 resolution: `claude-sonnet-5` role-playing a master scheduler, -defined by [situation-pack.md](situation-pack.md). The pack was authored from the use case's +defined by +[situation-pack.md](../../../cases/process-model-elicitation/baseline/situation-pack.md). The pack +was authored from the use case's operational prose (problem & context, data requirements, commercial angle) and never from the model outline β€” pack and reference model sit on opposite sides of the information wall. Facts are tiered: freely given, _(tacit)_ (surfaces only under reaching questions), _(believes)_ @@ -50,13 +54,15 @@ rather than fill). Rerun from the HASH root with `turbo run baseline:run --filter '@hashintel/brunch-agent' -- 1` / `turbo run baseline:run --filter '@hashintel/brunch-agent' -- 2` (needs `ANTHROPIC_API_KEY`). -Transcripts land in `transcripts/` and are committed as the experiment artifact. +Production transcripts land in +`docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/`. Tests set +`BRUNCH_BASELINE_TEST_OUTPUT_DIR` to an isolated directory and never write committed evidence. ## Instruments (scored in the read-out) 1. **Bano/Ferrari 34-mistake taxonomy**, via the operationalized Likert questionnaire (verbatim in - [../research/re-interviewing-literature-worker-report.md](../research/re-interviewing-literature-worker-report.md)), + [interviewing-literature-source-catalog.md](../../../../docs/reference/research/elicitation/interviewing-literature-source-catalog.md)), scored per LLMREI practice: Question Formulation, Question Omission, Order of Interview, Communication Skills, Customer Interaction (Analyst Behaviour and Teamwork & Planning dropped as inapplicable to a text-only single agent). diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/run.ts b/libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/run.ts similarity index 95% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/run.ts rename to libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/run.ts index 4015cfefe5c..4ec4b0bae3e 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/run.ts +++ b/libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/run.ts @@ -2,7 +2,7 @@ // // Runs one condition of the two-condition experiment: an interviewer model is asked to // "interview me, then produce the model" against a simulated domain expert defined by -// situation-pack.md. Condition 1 sends no interviewer system prompt (bare Claude); +// the baseline case's situation-pack.md. Condition 1 sends no interviewer system prompt (bare Claude); // condition 2 sends v0-prompt.md (the seven-category elicitation surface as guidance). // // Usage: ANTHROPIC_API_KEY=... node --experimental-strip-types run.ts <1|2> [--resume|--continue-final] @@ -10,7 +10,7 @@ // --continue-final ask the interviewer to finish a final delivery that was cut off at // max_tokens, and rewrite the artifacts with the merged message // -// Outputs, under transcripts/: +// Production outputs, under docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/: // condition-.md readable transcript with run metadata // condition-.raw.json full message arrays + per-call token usage (also the checkpoint) // condition--model.txt the final delivery message, verbatim (delivered runs only) @@ -75,6 +75,13 @@ interface RawCheckpoint { } const clientModule = process.env["BRUNCH_BASELINE_ANTHROPIC_MODULE"]; +const testOutputDirectory = process.env["BRUNCH_BASELINE_TEST_OUTPUT_DIR"]; +if (testOutputDirectory && !clientModule) { + console.error( + "BRUNCH_BASELINE_TEST_OUTPUT_DIR requires BRUNCH_BASELINE_ANTHROPIC_MODULE", + ); + process.exit(1); +} const apiKey = process.env["ANTHROPIC_API_KEY"]; if (!apiKey && !clientModule) { console.error("ANTHROPIC_API_KEY is not set"); @@ -109,7 +116,20 @@ if (!["fresh", "--resume", "--continue-final"].includes(mode)) usage(); const condition: "1" | "2" = conditionArg; const baseDir = fileURLToPath(new URL(".", import.meta.url)); -const transcriptDir = `${baseDir}transcripts`; +const caseDir = fileURLToPath( + new URL( + "../../../cases/process-model-elicitation/baseline/", + import.meta.url, + ), +); +const transcriptDir = + testOutputDirectory ?? + fileURLToPath( + new URL( + "../../../../docs/evidence/evaluations/process-model-elicitation/baseline/transcripts/", + import.meta.url, + ), + ); const rawPath = `${transcriptDir}/condition-${condition}.raw.json`; const calls: CallRecord[] = []; @@ -211,7 +231,10 @@ async function callInterviewer( } async function loadSection(file: string): Promise { - const raw = await readFile(baseDir + file, "utf8"); + const raw = await readFile( + file.startsWith("/") ? file : baseDir + file, + "utf8", + ); const separatorIndex = raw.indexOf("\n---\n"); return separatorIndex === -1 ? raw.trim() @@ -240,10 +263,10 @@ async function isFinalModel(message: string): Promise { return verdict.text.trim().toUpperCase().startsWith("YES"); } -const openingMessage = await loadSection("opening-message.md"); +const openingMessage = await loadSection(`${caseDir}opening-message.md`); const v0Prompt = condition === "2" ? await loadSection("v0-prompt.md") : undefined; -const situationPack = await readFile(baseDir + "situation-pack.md", "utf8"); +const situationPack = await readFile(`${caseDir}situation-pack.md`, "utf8"); await mkdir(transcriptDir, { recursive: true }); let interviewerMessages: ChatMessage[] = [ diff --git a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/v0-prompt.md b/libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/v0-prompt.md similarity index 100% rename from libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/v0-prompt.md rename to libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/v0-prompt.md diff --git a/libs/@hashintel/brunch-agent/packages/core/package.json b/libs/@hashintel/brunch-agent/packages/core/package.json index f9001fcbe9c..76258ade034 100644 --- a/libs/@hashintel/brunch-agent/packages/core/package.json +++ b/libs/@hashintel/brunch-agent/packages/core/package.json @@ -24,7 +24,7 @@ } }, "scripts": { - "baseline:run": "node --experimental-strip-types ../../docs/planning/process-model-elicitation/baseline/run.ts", + "baseline:run": "node --experimental-strip-types ../../evaluations/protocols/process-model-elicitation/baseline/run.ts", "build": "vite build", "fix:eslint": "oxlint --fix --type-aware --report-unused-disable-directives-severity=error .", "linear:graph": "node --experimental-strip-types ../../scripts/linear-project-graph.ts", diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/baseline-runner.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/baseline-runner.test.ts index 93f38d9a23e..01fb800398f 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/baseline-runner.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/architecture/baseline-runner.test.ts @@ -1,5 +1,7 @@ import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; import { cp, mkdtemp, readFile, rm } from "node:fs/promises"; +import { createServer } from "node:http"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -8,9 +10,13 @@ import { afterEach, describe, expect, test } from "vitest"; import { CONTEXT_ROOT, contextRootPresent } from "./workspace"; -const BASELINE_DIR = join( +const BASELINE_PROTOCOL_DIR = join( CONTEXT_ROOT, - "docs/planning/process-model-elicitation/baseline", + "evaluations/protocols/process-model-elicitation/baseline", +); +const BASELINE_CASE_DIR = join( + CONTEXT_ROOT, + "evaluations/cases/process-model-elicitation/baseline", ); const STUB_MODULE = pathToFileURL( join(import.meta.dirname, "fixtures/baseline-anthropic-stub.ts"), @@ -22,19 +28,36 @@ interface StubReply { truncated?: boolean; } -async function createBaselineCopy(): Promise { +interface BaselineCopy { + outputDirectory: string; + protocolDirectory: string; + testDirectory: string; +} + +async function createBaselineCopy(): Promise { const testDirectory = await mkdtemp(join(tmpdir(), "baseline-runner-test-")); temporaryDirectories.push(testDirectory); - await cp(BASELINE_DIR, testDirectory, { recursive: true }); - await rm(join(testDirectory, "transcripts"), { - recursive: true, - force: true, - }); - return testDirectory; + const protocolDirectory = join( + testDirectory, + "evaluations/protocols/process-model-elicitation/baseline", + ); + const caseDirectory = join( + testDirectory, + "evaluations/cases/process-model-elicitation/baseline", + ); + await Promise.all([ + cp(BASELINE_PROTOCOL_DIR, protocolDirectory, { recursive: true }), + cp(BASELINE_CASE_DIR, caseDirectory, { recursive: true }), + ]); + return { + outputDirectory: join(testDirectory, "test-output"), + protocolDirectory, + testDirectory, + }; } async function runBaseline( - testDirectory: string, + baselineCopy: BaselineCopy, replies: StubReply[], mode?: "--resume" | "--continue-final", ): Promise<{ @@ -50,15 +73,21 @@ async function runBaseline( stderr: string; requests: Array<{ messages: Array> }>; }> { - const requestsPath = join(testDirectory, "requests.jsonl"); + const requestsPath = join(baselineCopy.testDirectory, "requests.jsonl"); const subprocess = spawn( process.execPath, - ["--experimental-strip-types", "run.ts", "1", ...(mode ? [mode] : [])], + [ + "--experimental-strip-types", + join(baselineCopy.protocolDirectory, "run.ts"), + "1", + ...(mode ? [mode] : []), + ], { - cwd: testDirectory, + cwd: baselineCopy.testDirectory, env: { ...process.env, BRUNCH_BASELINE_ANTHROPIC_MODULE: STUB_MODULE, + BRUNCH_BASELINE_TEST_OUTPUT_DIR: baselineCopy.outputDirectory, BASELINE_STUB_REPLIES: JSON.stringify(replies), BASELINE_STUB_REQUESTS_PATH: requestsPath, }, @@ -78,7 +107,7 @@ async function runBaseline( const checkpoint = JSON.parse( await readFile( - join(testDirectory, "transcripts/condition-1.raw.json"), + join(baselineCopy.outputDirectory, "condition-1.raw.json"), "utf8", ), ); @@ -100,6 +129,61 @@ afterEach(async () => { describe.skipIf(!contextRootPresent)( "baseline runner completion metadata", () => { + test("rejects an output override without the stub module before API calls or output", async () => { + const baselineCopy = await createBaselineCopy(); + let apiCalls = 0; + const server = createServer((_request, response) => { + apiCalls += 1; + response.writeHead(500).end(); + }); + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected the test API server to listen on a TCP port"); + } + + const { BRUNCH_BASELINE_ANTHROPIC_MODULE: _stubModule, ...env } = + process.env; + const subprocess = spawn( + process.execPath, + [ + "--experimental-strip-types", + join(baselineCopy.protocolDirectory, "run.ts"), + "1", + ], + { + cwd: baselineCopy.testDirectory, + env: { + ...env, + ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`, + BRUNCH_BASELINE_TEST_OUTPUT_DIR: baselineCopy.outputDirectory, + }, + stdio: ["ignore", "ignore", "pipe"], + }, + ); + subprocess.stderr.setEncoding("utf8"); + let stderr = ""; + subprocess.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + const exitCode = await new Promise((resolve, reject) => { + subprocess.once("error", reject); + subprocess.once("close", resolve); + }); + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + + expect(exitCode).toBe(1); + expect(stderr).toContain( + "BRUNCH_BASELINE_TEST_OUTPUT_DIR requires BRUNCH_BASELINE_ANTHROPIC_MODULE", + ); + expect(apiCalls).toBe(0); + expect(existsSync(baselineCopy.outputDirectory)).toBe(false); + }); + test("checkpoints a truncated expert reply and stops before another interviewer call", async () => { const testDirectory = await createBaselineCopy(); const result = await runBaseline(testDirectory, [ @@ -179,7 +263,7 @@ describe.skipIf(!contextRootPresent)( { text: "part-5", truncated: true }, { text: "YES" }, ]); - await rm(join(testDirectory, "requests.jsonl")); + await rm(join(testDirectory.testDirectory, "requests.jsonl")); const continued = await runBaseline( testDirectory, diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/boundaries.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/boundaries.test.ts index 9f447276438..5d15cdfcd9b 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/boundaries.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/architecture/boundaries.test.ts @@ -49,7 +49,10 @@ test("every workspace package is one the spec topology names", () => { // spec: a package the spec does not name is loud, while an // intended-but-unbuilt one is not a failure. (The old hardcoded equality // would have failed the next legitimate package instead of governing it.) - const spec = readFileSync(join(CONTEXT_ROOT, "docs/spec.md"), "utf8"); + const spec = readFileSync( + join(CONTEXT_ROOT, "docs/specs/elicitation-kernel.md"), + "utf8", + ); const topology = /### 12\.2[^\n]*\n[\s\S]*?```text\n([\s\S]*?)```/.exec( spec, )?.[1]; diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/control-surfaces.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/control-surfaces.test.ts new file mode 100644 index 00000000000..9e27a187488 --- /dev/null +++ b/libs/@hashintel/brunch-agent/packages/core/test/architecture/control-surfaces.test.ts @@ -0,0 +1,101 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, test } from "vitest"; + +import { contextRootPresent, REPO_ROOT } from "./workspace"; + +const CONTROL_ROOT = join(REPO_ROOT, "docs/control"); +const STEERING_PATH = join(CONTROL_ROOT, "STEERING.md"); +const STRATEGY_LOG_PATH = join(CONTROL_ROOT, "STRATEGY-LOG.md"); +const REQUIRED_FIELDS = [ + "Date", + "Trigger/evidence", + "Decision", + "Consequences/cuts", + "Revisit when", + "Supersedes", + "Evidence links", +] as const; + +interface StrategyEntry { + readonly id: string; + readonly fields: ReadonlyMap; +} + +const strategyEntries = (markdown: string): StrategyEntry[] => + [ + ...markdown.matchAll( + /^### (S-\d{3})\n([\s\S]*?)(?=^### S-\d{3}\n|(?![\s\S]))/gm, + ), + ].map(([, id, body]) => ({ + id: id!, + fields: new Map( + [ + ...body!.matchAll( + /^\*\*([^*]+):\*\*\s*([\s\S]*?)(?=^\*\*[^*]+:\*\*|(?![\s\S]))/gm, + ), + ].map(([, name, value]) => [name!, value!.trim()]), + ), + })); + +describe.skipIf(!contextRootPresent)("strategic control surfaces", () => { + const steering = readFileSync(STEERING_PATH, "utf8"); + const strategyLog = readFileSync(STRATEGY_LOG_PATH, "utf8"); + const entries = strategyEntries(strategyLog); + + test("has one current mutable strategic control and no coordination control", () => { + expect(existsSync(STEERING_PATH)).toBe(true); + expect(existsSync(join(CONTROL_ROOT, "COORDINATION.md"))).toBe(false); + expect(steering).toContain("one mutable current strategic control"); + }); + + test("strategy entries all parse with monotonically increasing unique IDs", () => { + const entryArea = strategyLog.match(/^## Entries\n([\s\S]*)$/m); + expect(entryArea).not.toBeNull(); + const entryHeadingCount = [...entryArea![1]!.matchAll(/^### .+$/gm)].length; + expect(entries).toHaveLength(entryHeadingCount); + expect(entries.length).toBeGreaterThan(0); + expect(new Set(entries.map(({ id }) => id)).size).toBe(entries.length); + const numbers = entries.map(({ id }) => Number(id.slice(2))); + expect(numbers).toEqual([...numbers].sort((left, right) => left - right)); + }); + + test("strategy entries have every required field", () => { + for (const entry of entries) { + expect([...entry.fields.keys()]).toEqual(REQUIRED_FIELDS); + for (const value of entry.fields.values()) expect(value).not.toBe(""); + } + }); + + test("supersedes targets resolve backward without cycles", () => { + const seen = new Set(); + for (const entry of entries) { + const supersedes = entry.fields.get("Supersedes")!; + if (supersedes !== "none") { + expect(supersedes).toMatch(/^S-\d{3}$/); + expect(seen.has(supersedes)).toBe(true); + } + seen.add(entry.id); + } + }); + + test("every strategy ID in steering resolves and is unsuperseded", () => { + const governingLine = steering.match( + /Governing strategic decisions:([\s\S]*?)\n\n/, + ); + expect(governingLine).not.toBeNull(); + expect(governingLine![1]).toMatch(/S-\d{3}/); + const referencedIds = [...steering.matchAll(/S-\d{3}/g)].map(([id]) => id); + const knownIds = new Set(entries.map(({ id }) => id)); + const supersededIds = new Set( + entries + .map(({ fields }) => fields.get("Supersedes")!) + .filter((id) => id !== "none"), + ); + for (const id of referencedIds) { + expect(knownIds.has(id)).toBe(true); + expect(supersededIds.has(id)).toBe(false); + } + }); +}); diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/docs-index.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/docs-index.test.ts index ae42774bfa5..db2d443be41 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/docs-index.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/architecture/docs-index.test.ts @@ -2,8 +2,8 @@ * Documentation coverage, as tests rather than as a habit. * * `docs/agents/documentation.md` states the placement rules as obligations β€” - * every document is indexed, living cross-effort docs go in - * `docs/planning/_shared/`, agent protocols are reachable from `AGENTS.md`. An + * every document is indexed, documents occupy their role-based authority zone, + * and agent protocols are reachable from `AGENTS.md`. An * obligation nobody can run is a wish, and this one decays in the quietest way * available: a file lands, the INDEX pass is skipped once, and nothing ever says * so. These are the mechanical checks β€” they walk the real `docs/` tree, so a @@ -41,6 +41,11 @@ const INDEX_EXEMPT = ["agents", INDEX_RELPATH]; * navigation. */ const LINK_CHECK_EXEMPT = ["reference/amp-analysis-flue-vs-tilde.md"]; +/** Immutable migration snapshots whose old paths are part of the evidence. */ +const LINK_CHECK_EXEMPT_PREFIXES = [ + "archive/migrations/hash-monorepo-import-plan.md", + "archive/migrations/issue-pr-legibility-2026-08-20/", +]; /** Posix-shaped, because that is how a markdown link target is written. */ const relPath = (path: string): string => @@ -173,7 +178,12 @@ describe.skipIf(!contextRootPresent)( test("relative links in context documentation point at existing files", () => { for (const file of FILES) { - if (!file.endsWith(".md") || LINK_CHECK_EXEMPT.includes(file)) continue; + if ( + !file.endsWith(".md") || + LINK_CHECK_EXEMPT.includes(file) || + LINK_CHECK_EXEMPT_PREFIXES.some((prefix) => file.startsWith(prefix)) + ) + continue; const sourcePath = join(DOCS_ROOT, file); const lines = readFileSync(sourcePath, "utf8").split("\n"); @@ -209,17 +219,14 @@ test("relative links in context documentation point at existing files", () => { } }); -test("docs/planning/ holds only effort directories", () => { - // Placement rule: a cross-effort living document goes in `_shared/`, and a - // record of one effort goes in that effort's directory. A file loose at the - // top level is the state both of those replaced β€” it belongs to everything and - // so to nothing, and it is where the last round of drift accumulated. - const planning = join(DOCS_ROOT, "planning"); - const loose = readdirSync(planning) - .filter((entry) => !SKIP_FILES.includes(entry)) - .filter((entry) => !statSync(join(planning, entry)).isDirectory()) - .sort(); - expect(loose).toEqual([]); +test("retired planning and history paths contain no documents", () => { + expect( + FILES.filter( + (file) => file.startsWith("planning/") || file.startsWith("history/"), + ), + ).toEqual([]); + expect(existsSync(join(DOCS_ROOT, "planning"))).toBe(false); + expect(existsSync(join(DOCS_ROOT, "history"))).toBe(false); }); describe.skipIf(!contextRootPresent)( diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts index c8ceb357d60..c646d15dc0b 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts @@ -20,19 +20,94 @@ interface ProjectIssue { readonly title: string; readonly stateName: string; readonly parentIdentifier?: string; + readonly assigneeName?: string; + readonly assignedToViewer: boolean; readonly external: boolean; } interface ProjectGraph { readonly projectName: string; + readonly viewerName: string; + readonly includeClosed: boolean; readonly issues: readonly ProjectIssue[]; readonly hardEdges: readonly { readonly from: string; readonly to: string }[]; } interface LinearProjectGraphModule { readonly renderProjectGraph: (graph: ProjectGraph) => string; + readonly readProjectIssuePage: (value: unknown) => ProjectIssuePage; + readonly fetchProjectGraph: ( + projectName: string, + includeClosed: boolean, + queryPage: (projectName: string, after: string | null) => ProjectIssuePage, + ) => ProjectGraph; + readonly parseArguments: (arguments_: readonly string[]) => { + readonly projectName: string; + readonly includeClosed: boolean; + readonly help: boolean; + }; } +interface ProjectIssuePage { + readonly projectName: string; + readonly viewer: { readonly id: string; readonly name: string }; + readonly issues: readonly LinearIssueRecord[]; + readonly hasNextPage: boolean; + readonly endCursor: string | null; +} + +interface LinearIssueRecord { + readonly identifier: string; + readonly title: string; + readonly state: { readonly name: string; readonly type: string }; + readonly project: { readonly name: string } | null; + readonly assignee?: { readonly id: string; readonly name: string } | null; + readonly parent: { readonly identifier: string } | null; + readonly relations: { + readonly pageInfo: { readonly hasNextPage: boolean }; + readonly nodes: readonly []; + }; + readonly inverseRelations: { + readonly pageInfo: { readonly hasNextPage: boolean }; + readonly nodes: readonly []; + }; +} + +const issue = ( + identifier: string, + assignee: LinearIssueRecord["assignee"], + type = "started", +): LinearIssueRecord => ({ + identifier, + title: identifier, + state: { name: type === "completed" ? "Done" : "In progress", type }, + project: { name: "brunch-agent" }, + assignee, + parent: null, + relations: { pageInfo: { hasNextPage: false }, nodes: [] }, + inverseRelations: { pageInfo: { hasNextPage: false }, nodes: [] }, +}); + +const response = ( + viewer: unknown, + issues: readonly LinearIssueRecord[] = [], +) => ({ + data: { + viewer, + projects: { + nodes: [ + { + name: "brunch-agent", + issues: { + pageInfo: { hasNextPage: false, endCursor: null }, + nodes: issues, + }, + }, + ], + }, + }, +}); + async function loadRenderProjectGraph(): Promise< LinearProjectGraphModule["renderProjectGraph"] > { @@ -41,16 +116,133 @@ async function loadRenderProjectGraph(): Promise< } describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { + test("parses viewer identity separately from its display name", async () => { + const { readProjectIssuePage } = (await import( + SCRIPT_MODULE_URL + )) as LinearProjectGraphModule; + const page = readProjectIssuePage( + response({ id: "viewer-id", name: "Same Display Name" }, [ + issue("FE-1", { id: "other-id", name: "Same Display Name" }), + ]), + ); + expect(page.viewer).toEqual({ id: "viewer-id", name: "Same Display Name" }); + }); + + test.each([undefined, null, {}, { id: "", name: "Lu" }])( + "rejects missing or malformed viewer: %j", + async (viewer) => { + const { readProjectIssuePage } = (await import( + SCRIPT_MODULE_URL + )) as LinearProjectGraphModule; + expect(() => readProjectIssuePage(response(viewer))).toThrow( + "missing or malformed authenticated viewer", + ); + }, + ); + + test("classifies viewer, wrong, unassigned, null, and absent assignees by ID", async () => { + const { readProjectIssuePage, fetchProjectGraph } = (await import( + SCRIPT_MODULE_URL + )) as LinearProjectGraphModule; + const parsed = readProjectIssuePage( + response({ id: "viewer-id", name: "Lu" }, [ + issue("FE-1", { id: "viewer-id", name: "Lu" }), + issue("FE-2", { id: "other-id", name: "Other" }), + issue("FE-3", null), + issue("FE-4", undefined), + ]), + ); + const graph = fetchProjectGraph("brunch-agent", false, () => parsed); + expect( + graph.issues.map(({ identifier, assignedToViewer, assigneeName }) => ({ + identifier, + assignedToViewer, + assigneeName, + })), + ).toEqual([ + { identifier: "FE-1", assignedToViewer: true, assigneeName: "Lu" }, + { identifier: "FE-2", assignedToViewer: false, assigneeName: "Other" }, + { identifier: "FE-3", assignedToViewer: false, assigneeName: undefined }, + { identifier: "FE-4", assignedToViewer: false, assigneeName: undefined }, + ]); + }); + + test("accumulates two pages and passes the returned cursor", async () => { + const { fetchProjectGraph } = (await import( + SCRIPT_MODULE_URL + )) as LinearProjectGraphModule; + const calls: Array = []; + const pages: ProjectIssuePage[] = [ + { + projectName: "brunch-agent", + viewer: { id: "viewer-id", name: "Lu" }, + issues: [issue("FE-1", { id: "viewer-id", name: "Lu" })], + hasNextPage: true, + endCursor: "next-page", + }, + { + projectName: "brunch-agent", + viewer: { id: "viewer-id", name: "Lu" }, + issues: [issue("FE-2", { id: "viewer-id", name: "Lu" })], + hasNextPage: false, + endCursor: null, + }, + ]; + const graph = fetchProjectGraph( + "brunch-agent", + false, + (_project, after) => { + calls.push(after); + return pages[calls.length - 1]!; + }, + ); + expect(calls).toEqual([null, "next-page"]); + expect(graph.issues.map(({ identifier }) => identifier)).toEqual([ + "FE-1", + "FE-2", + ]); + }); + + test("defaults to open issues and --all includes closed issues", async () => { + const { fetchProjectGraph, parseArguments, renderProjectGraph } = + (await import(SCRIPT_MODULE_URL)) as LinearProjectGraphModule; + const page: ProjectIssuePage = { + projectName: "brunch-agent", + viewer: { id: "viewer-id", name: "Lu" }, + issues: [ + issue("FE-1", { id: "viewer-id", name: "Lu" }), + issue("FE-2", { id: "viewer-id", name: "Lu" }, "completed"), + ], + hasNextPage: false, + endCursor: null, + }; + expect(parseArguments([]).includeClosed).toBe(false); + expect(parseArguments(["--all"]).includeClosed).toBe(true); + const openGraph = fetchProjectGraph("brunch-agent", false, () => page); + const allGraph = fetchProjectGraph("brunch-agent", true, () => page); + expect(openGraph.issues).toHaveLength(1); + expect(allGraph.issues).toHaveLength(2); + expect(renderProjectGraph(openGraph)).toContain( + "project brunch-agent open=1", + ); + expect(renderProjectGraph(allGraph)).toContain( + "project brunch-agent issues=2", + ); + }); + test("renders hard-dependency layers with enough issue context for agent inference", async () => { const renderProjectGraph = await loadRenderProjectGraph(); const graph: ProjectGraph = { projectName: "brunch-agent", + viewerName: "Lu Nelson", + includeClosed: false, issues: [ { identifier: "FE-100", title: "Build the transport", stateName: "In progress", parentIdentifier: "FE-1", + assignedToViewer: true, external: false, }, { @@ -58,12 +250,15 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { title: "Return client tools", stateName: "Todo", parentIdentifier: "FE-1", + assigneeName: "Another Owner", + assignedToViewer: false, external: false, }, { identifier: "FE-102", title: "Add private sessions", stateName: "Todo", + assignedToViewer: true, external: false, }, { @@ -71,6 +266,7 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { title: "Ship the integration", stateName: "Todo", parentIdentifier: "FE-1", + assignedToViewer: true, external: false, }, ], @@ -81,12 +277,14 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { ], }; - expect(renderProjectGraph(graph)).toBe(`project brunch-agent open=4 hard=3 -legend: L=hard-dependency layer; p=parent; <=blocked by; =>blocks; *=outside project -L0 FE-100 [In progress p:FE-1] =>FE-101,FE-102 | Build the transport -L1 FE-101 [Todo p:FE-1] <=FE-100 =>FE-103 | Return client tools -L1 FE-102 [Todo root] <=FE-100 | Add private sessions -L2 FE-103 [Todo p:FE-1] <=FE-101 | Ship the integration + expect(renderProjectGraph(graph)) + .toBe(`project brunch-agent open=4 hard=3 assignee-mismatches=1 +viewer: Lu Nelson +legend: L=hard-dependency layer; p=parent; a=assignee; <=blocked by; =>blocks; *=outside project +L0 FE-100 [In progress p:FE-1 a:self] =>FE-101,FE-102 | Build the transport +L1 FE-101 [Todo p:FE-1 a:Another Owner] <=FE-100 =>FE-103 | Return client tools +L1 FE-102 [Todo root a:self] <=FE-100 | Add private sessions +L2 FE-103 [Todo p:FE-1 a:self] <=FE-101 | Ship the integration cycles: none`); }); @@ -94,17 +292,21 @@ cycles: none`); const renderProjectGraph = await loadRenderProjectGraph(); const graph: ProjectGraph = { projectName: "brunch-agent", + viewerName: "Lu Nelson", + includeClosed: false, issues: [ { identifier: "FE-100", title: "First issue", stateName: "Todo", + assignedToViewer: true, external: false, }, { identifier: "FE-101", title: "Second issue", stateName: "Todo", + assignedToViewer: false, external: true, }, ], diff --git a/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts b/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts index 470966adde3..7d8ee640ff7 100644 --- a/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts +++ b/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts @@ -7,6 +7,8 @@ export interface ProjectIssue { readonly title: string; readonly stateName: string; readonly parentIdentifier?: string; + readonly assigneeName?: string; + readonly assignedToViewer: boolean; readonly external: boolean; } @@ -17,6 +19,8 @@ export interface HardEdge { export interface ProjectGraph { readonly projectName: string; + readonly viewerName: string; + readonly includeClosed: boolean; readonly issues: readonly ProjectIssue[]; readonly hardEdges: readonly HardEdge[]; } @@ -31,6 +35,7 @@ interface LinearIssueRef { readonly title: string; readonly state: LinearState; readonly project: { readonly name: string } | null; + readonly assignee?: { readonly id: string; readonly name: string } | null; } interface LinearRelation { @@ -53,6 +58,7 @@ interface LinearIssueRecord extends LinearIssueRef { interface ProjectIssuePage { readonly projectName: string; + readonly viewer: { readonly id: string; readonly name: string }; readonly issues: readonly LinearIssueRecord[]; readonly hasNextPage: boolean; readonly endCursor: string | null; @@ -60,6 +66,7 @@ interface ProjectIssuePage { const PROJECT_QUERY = ` query ProjectGraph($project: String!, $after: String) { + viewer { id name } projects(filter: { name: { eq: $project } }, first: 2) { nodes { name @@ -71,6 +78,7 @@ query ProjectGraph($project: String!, $after: String) { state { name type } parent { identifier } project { name } + assignee { id name } relations(first: 15) { pageInfo { hasNextPage } nodes { @@ -196,10 +204,14 @@ export function renderProjectGraph(graph: ProjectGraph): string { ); const externalCount = graph.issues.filter((issue) => issue.external).length; const projectIssueCount = graph.issues.length - externalCount; + const assigneeMismatchCount = graph.issues.filter( + (issue) => !issue.external && !issue.assignedToViewer, + ).length; const header = [ `project ${graph.projectName}`, - `open=${projectIssueCount}`, + `${graph.includeClosed ? "issues" : "open"}=${projectIssueCount}`, `hard=${hardEdges.length}`, + `assignee-mismatches=${assigneeMismatchCount}`, ...(externalCount > 0 ? [`external=${externalCount}`] : []), ].join(" "); const issueLines = [...graph.issues] @@ -218,7 +230,13 @@ export function renderProjectGraph(graph: ProjectGraph): string { const tags = [ issue.stateName, issue.parentIdentifier ? `p:${issue.parentIdentifier}` : "root", - ...(issue.external ? ["*"] : []), + ...(issue.external + ? ["*"] + : [ + issue.assignedToViewer + ? "a:self" + : `a:${issue.assigneeName ?? "unassigned"}`, + ]), ].join(" "); const blockers = incoming.get(issue.identifier); const blocks = outgoing.get(issue.identifier); @@ -235,7 +253,8 @@ export function renderProjectGraph(graph: ProjectGraph): string { return [ header, - "legend: L=hard-dependency layer; p=parent; <=blocked by; =>blocks; *=outside project", + `viewer: ${graph.viewerName}`, + "legend: L=hard-dependency layer; p=parent; a=assignee; <=blocked by; =>blocks; *=outside project", ...issueLines, `cycles: ${cycles.length > 0 ? cycles.join(",") : "none"}`, ].join("\n"); @@ -252,9 +271,14 @@ const isLinearIssueRef = (value: unknown): value is LinearIssueRef => typeof value.state.name === "string" && typeof value.state.type === "string" && (value.project === null || - (isRecord(value.project) && typeof value.project.name === "string")); - -const readProjectIssuePage = (value: unknown): ProjectIssuePage => { + (isRecord(value.project) && typeof value.project.name === "string")) && + (value.assignee === undefined || + value.assignee === null || + (isRecord(value.assignee) && + typeof value.assignee.id === "string" && + typeof value.assignee.name === "string")); + +export const readProjectIssuePage = (value: unknown): ProjectIssuePage => { if (!isRecord(value)) throw new Error("Linear returned a non-object response."); if (Array.isArray(value.errors) && value.errors.length > 0) { @@ -270,6 +294,18 @@ const readProjectIssuePage = (value: unknown): ProjectIssuePage => { ); } const data = value.data; + const viewer = isRecord(data) ? data.viewer : undefined; + if ( + !isRecord(viewer) || + typeof viewer.id !== "string" || + viewer.id.length === 0 || + typeof viewer.name !== "string" || + viewer.name.length === 0 + ) { + throw new Error( + "Linear returned a missing or malformed authenticated viewer (expected non-empty id and name).", + ); + } const projects = isRecord(data) && isRecord(data.projects) ? data.projects.nodes : undefined; if ( @@ -303,6 +339,11 @@ const readProjectIssuePage = (value: unknown): ProjectIssuePage => { !isRecord(issue.state) || typeof issue.state.name !== "string" || typeof issue.state.type !== "string" || + (issue.assignee !== undefined && + issue.assignee !== null && + (!isRecord(issue.assignee) || + typeof issue.assignee.id !== "string" || + typeof issue.assignee.name !== "string")) || !isRecord(issue.relations) || !isRecord(issue.inverseRelations) || !Array.isArray(issue.relations.nodes) || @@ -357,6 +398,7 @@ const readProjectIssuePage = (value: unknown): ProjectIssuePage => { } return { projectName: project.name, + viewer: { id: viewer.id, name: viewer.name }, issues, hasNextPage: issuesConnection.pageInfo.hasNextPage, endCursor, @@ -392,26 +434,35 @@ const queryProjectPage = ( const asProjectIssue = ( issue: LinearIssueRef, projectName: string, + viewerId: string, parentIdentifier?: string, ): ProjectIssue => ({ identifier: issue.identifier, title: issue.title, stateName: issue.state.name, ...(parentIdentifier ? { parentIdentifier } : {}), + ...(issue.assignee ? { assigneeName: issue.assignee.name } : {}), + assignedToViewer: issue.assignee?.id === viewerId, external: issue.project?.name !== projectName, }); export const fetchProjectGraph = ( projectName: string, includeClosed = false, + queryPage: ( + projectName: string, + after: string | null, + ) => ProjectIssuePage = queryProjectPage, ): ProjectGraph => { const projectIssues = new Map(); let after: string | null = null; let resolvedProjectName = projectName; + let viewer = { id: "", name: "unknown" }; do { - const page = queryProjectPage(projectName, after); + const page = queryPage(projectName, after); resolvedProjectName = page.projectName; + viewer = page.viewer; for (const issue of page.issues) projectIssues.set(issue.identifier, issue); after = page.hasNextPage ? page.endCursor : null; if (page.hasNextPage && after === null) { @@ -428,7 +479,12 @@ export const fetchProjectGraph = ( if (!selected(issue)) continue; issues.set( issue.identifier, - asProjectIssue(issue, resolvedProjectName, issue.parent?.identifier), + asProjectIssue( + issue, + resolvedProjectName, + viewer.id, + issue.parent?.identifier, + ), ); for (const relation of issue.relations.nodes) { @@ -442,7 +498,7 @@ export const fetchProjectGraph = ( issues.set( relatedIssue.identifier, issues.get(relatedIssue.identifier) ?? - asProjectIssue(relatedIssue, resolvedProjectName), + asProjectIssue(relatedIssue, resolvedProjectName, viewer.id), ); const edge = { from: issue.identifier, to: relatedIssue.identifier }; hardEdges.set(`${edge.from}>${edge.to}`, edge); @@ -454,7 +510,7 @@ export const fetchProjectGraph = ( issues.set( sourceIssue.identifier, issues.get(sourceIssue.identifier) ?? - asProjectIssue(sourceIssue, resolvedProjectName), + asProjectIssue(sourceIssue, resolvedProjectName, viewer.id), ); const edge = { from: sourceIssue.identifier, to: issue.identifier }; hardEdges.set(`${edge.from}>${edge.to}`, edge); @@ -463,6 +519,8 @@ export const fetchProjectGraph = ( return { projectName: resolvedProjectName, + viewerName: viewer.name, + includeClosed, issues: [...issues.values()], hardEdges: [...hardEdges.values()], }; @@ -474,7 +532,7 @@ Print a compact, read-only hard-dependency projection for agent sequencing. Defaults to open issues in the brunch-agent project. The output is factual input; it does not infer priority, soft dependencies, or a sequencing recommendation.`; -const parseArguments = ( +export const parseArguments = ( arguments_: readonly string[], ): { readonly projectName: string; diff --git a/oxfmt.config.ts b/oxfmt.config.ts index 62aaa3398dc..499253ac6a0 100644 --- a/oxfmt.config.ts +++ b/oxfmt.config.ts @@ -59,9 +59,8 @@ export default defineConfig({ ignorePatterns: [ // License files have intentional formatting that shouldn't be touched "**/LICENSE*.md", - // Imported Brunch context docs, including hash-pinned frozen data - // snapshots whose bytes must not be rewritten - "libs/@hashintel/brunch-agent/docs/**", + // Brunch Markdown is created and maintained by agents + "libs/@hashintel/brunch-agent/**/*.md", // While supported, it doesn't yet support the full breadth // of taplo configuration options that we have set // see: https://github.com/oxc-project/oxc/issues/18580