From d04d77ee5190cf4b318d09b25ec74244e3d8f1c7 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 12:32:21 +0200 Subject: [PATCH 01/11] Add September steering model Define the bounded CPS review-and-revise proof and record its objective, acceptance path, gates, beliefs, cuts, and stop conditions in a mutable steering control. Reconcile coordination around parallel semantic and reviewer lanes so issue availability no longer substitutes for strategic priority. Amp-Thread-ID: https://ampcode.com/threads/T-01a032f5-37ff-7328-a0d5-f8ee48678dd2 Co-authored-by: Amp --- libs/@hashintel/brunch-agent/docs/INDEX.md | 1 + .../docs/planning/_shared/COORDINATION.md | 98 +++--- .../docs/planning/_shared/STEERING.md | 296 ++++++++++++++++++ 3 files changed, 358 insertions(+), 37 deletions(-) create mode 100644 libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md diff --git a/libs/@hashintel/brunch-agent/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index ca6ea309c02..b0834155a7d 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -71,6 +71,7 @@ _(empty — items settle out via the arc-close inbox sweep)_ | [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 | +| [STEERING](planning/_shared/STEERING.md) | active | FE-1357 / FE-1476 | Current objective-driven strategy for the September CPS review-and-revise proof: evidence, architectural pressure, proof frontiers, cuts, issue projection, risks, and replan conditions | | [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 | diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md b/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md index 54e343fb836..871a5ad3891 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md +++ b/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md @@ -2,9 +2,10 @@ 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. +Linear is canonical for issue state, parentage, and hard `blocks` relations. The +[steering model](./STEERING.md) chooses the current objective, proof frontiers, and cuts under +pressure. This file projects that strategy onto mechanically available work: 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 @@ -13,19 +14,22 @@ honest recommendation. Do not paste the generated graph here or mirror issue sta ## 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. +As of **2026-08-24**, FE-1476 (the September demo delivery) changes the recommendation from generic +package completion to a concrete CPS review-and-revise proof. After FE-1437 (the monorepo import) +lands, open two fronts in parallel. The semantic front starts FE-1482 (the CPS plugin) against one +worked fixture and settles FE-1480's requirements-model-to-SDCPN authority boundary before it +implements a projector; FE-1478 (net-to-requirements provenance) is part of that spine from its +first types. The experience front advances FE-1438 (machine client-tool round-trip) and FE-1439 +(private sessions) far enough for a new reviewer conversation to target an existing document, +while FE-1477/FE-1440 share one provider-routing implementation. Join the fronts at FE-1479 +(targeted re-elicitation), then drive the same path through FE-1423's pre-exposure gates and +FE-1441 deployment. + +FE-1393's generic Gherkin artifact and FE-1387's second-target contract freeze no longer gate the +September proof. FE-1402, FE-1403, FE-1406, and FE-1431 supply only the completion, guidance, +strategy, and contract slices the CPS `review-and-revise` runbook consumes. FE-1481 selects +YAML/Markdown export as the requirements-model inspection floor; broad UI follows only if the +closed loop is already proved. ```text legend: @@ -36,29 +40,30 @@ legend: 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 + FE-1476 [objective] # September reviewer demo + FE-1482 [next, semantic] # concrete CPS plugin + review/revise runbook + FE-1480 [decision, semantic] # model/projection authority, then projector + FE-1478 [semantic proof] # provenance through all three registers + FE-1438 [next, experience] # machine client-tool round-trip + application + FE-1439 [next, experience] # existing-target reviewer session ownership + FE-1477/FE-1440 [experience] # one provider-routing implementation + FE-1479 [join] # targeted correction changes the live net + FE-1481 [fallback] # structured model export before UI + FE-1441 [deployed proof] # HASH deployment 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 + FE-1437 branch on main -[state-gate]-> FE-1438, FE-1439 + FE-1480 decision -[input]-> FE-1482, FE-1478 + FE-1402, FE-1403, + FE-1406, FE-1431 -[input]-> FE-1482 + FE-1482, FE-1478, + FE-1438, FE-1439 -[input]-> FE-1479 + FE-1479, FE-1440 -[input]-> FE-1441 ``` Hard-edge truth remains in Linear. The graph above is a deliberately focused recommendation, @@ -90,8 +95,18 @@ 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. +- **Projection authority — FE-1480.** The ticket assumes non-deterministic LLM inference from the + requirements model to SDCPN, while ADR-0003 requires write-time-only semantic inference and a + pure projection. A worked CPS transformation must assign every judgment to capture, fold, + projection, or document application before the interface freezes. +- **Controller and runbook.** The harness does not read the folded model or open issues back into + the agent, and no plugin defines a job trajectory or stopping rule. FE-1482 must exercise the + narrow `review-and-revise` loop; FE-1406 and FE-1402/FE-1403 are inputs, not parallel products. +- **Reviewer target identity — FE-1439 × FE-1479.** The current host derives target-document + identity from conversation identity. September requires a new reviewer conversation against an + existing target without weakening owner isolation. +- **Contract freeze — FE-1387.** The CPS target must stress the plugin contract before it freezes. + The freeze follows the September semantic proof rather than gating it. - **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. @@ -112,11 +127,20 @@ field. ADR-0003 keeps field-level structure below the capture's single epistemic ## Exceptional roots -These project issues intentionally have no parent and are roots under the registry rule: +These project issues currently have no parent. Some are intentional roots; the temporary or +unresolved roots are named here until their Linear parentage is settled: -- **FE-1331 — start elicitation from Petrinaut's create-new-net flow.** Deferred post-September - consumer topology; returns after in-Petrinaut staging proves itself. +- **FE-1331 — start elicitation from Petrinaut's create-new-net flow.** ADR-0004 un-deferred this as + September topology, while FE-1476's new reviewer scenario starts from an existing target. Keep + the conflict visible until Dora confirms the use case and the ADR is amended if necessary. - **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. +- **FE-1472 — evaluate the nested Anthropic SDK pin.** Unrelated triage root; no owning delivery + map has been chosen. +- **FE-1476 — prepare the September demo.** Temporary delivery root pending the recommended fold + under FE-1357. +- **FE-1477 through FE-1482 — September outcome slices.** PM-authored issues adopted by the + steering model but not yet folded in Linear. The recommended parent is FE-1476; overlaps and + ownership boundaries are recorded in STEERING's issue projection before external mutation. diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md b/libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md new file mode 100644 index 00000000000..45f5abbb530 --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md @@ -0,0 +1,296 @@ +# Brunch steering model + +This is the current strategic understanding of Brunch: what outcome matters now, what the system +can actually do, which beliefs remain weak, and where to spend the next unit of time. Its immediate +decision horizon is the September demo, but it steers the whole context rather than one package or +one Linear map. + +This is not a roadmap, a specification, or an issue mirror. Specifications and accepted ADRs define +obligations and settled architecture. Linear is canonical for issue state, hierarchy, and hard +blockers. [COORDINATION](./COORDINATION.md) projects this strategy onto the mechanically available +work. This document owns the judgment between them: which objective to pursue under the current +time, evidence, risk, and product pressure; what to defer; and what new evidence would change that +choice. It may expose a conflict with a specification or ADR, but cannot silently override one. + +The document remains live. A steering pass orients from current evidence, chooses a frontier, +executes to a named proof, reconciles the result with specifications and issues, then replans only +where the evidence changed. Git carries the history; this file carries only the current model. Its +shape is intentionally specific to the present effort. Do not extract a generic template or skill +until a second real planning cycle shows which parts recur. + +## The September outcome + +As of **2026-08-24**, the working constraint is roughly two human weeks. The event date and final +business use case are not yet recorded here; Dora's confirmation is a decision gate, not an excuse +to leave the technical spine vague. + +FE-1476 (the September demo delivery) supplies the working scenario: + +1. A reviewer opens a prebuilt cyber-physical-process requirements model and its generated SDCPN in + Petrinaut. +2. The reviewer selects or describes a net element and asks why it was modelled that way. +3. Brunch traces the answer through the requirements model and captures to an exact source + utterance. +4. The reviewer scopes one correction and conducts three to five focused chat turns. +5. New or superseding captures change the elicited model; reprojection changes the corresponding + part of the live net without rebuilding unrelated parts. +6. The resulting artifact is handed to the existing optimisation experiment flow. + +This is a **review-and-revise** demonstration. It does not need to prove that Brunch can elicit an +entire CPS model from a blank conversation. It does need to prove a closed semantic and interaction +loop. A chat transcript beside a static fixture, an unexplained net mutation, or a test that injects +wiring absent from the deployed entrypoint does not satisfy the outcome. + +The proof spine is therefore: + +```text +source utterance + -> active typed capture + -> folded CPS requirements model + -> SDCPN element + provenance + -> reviewer question and scoped correction + -> superseding capture + -> changed folded model + -> changed live SDCPN + -> optimisation handoff +``` + +## Where the system actually stands + +The package topology is in place and the implemented tracer is real, but the September loop is not +an incremental extension of an almost-finished product. Most of the contract-bearing middle is +absent. + +| Surface | Evidence now | September consequence | +| --- | --- | --- | +| Ask, suspend, return | A user answer to `brunch_ask` survives the AI SDK/Flue boundary and resumes durable history. | Reuse; do not redesign the ask protocol. | +| Settlement and capture | A settled range is privately swept into quote-anchored captures and applied atomically. Supersession and active-head validation exist in the store. | Preserve as the evidence foundation, but expose active state to the controller. | +| Plugin SDK | The exported `Plugin` is deliberately only identity plus exactly one proposal type. Gherkin captures one verbatim statement. | There is no implemented fold, demand runner, model, projection, or useful hard-target plugin to extend. | +| Elicitation control | The agent receives general ask/sweep instructions. Sweep extraction sees a conversation range and proposal names only. No production path reads the active capture set or a derived model back into the interview. | Brunch cannot yet choose a next question from what it has learned or conduct a targeted correction. | +| CPS semantics | The three-register design and provisional two-schema/two-table plugin contract are desk-designed. No `plugin-cps` exists. | The critical semantic path must be built against a concrete CPS case, not inferred from Gherkin completeness. | +| Correction | The store can represent supersession, but extraction cannot see active capture IDs, model issues, or the target region; Gherkin cannot propose a supersession. | Targeted re-elicitation is structurally unreachable despite the storage mechanics being present. | +| Petrinaut transport | Local panel streaming and human ask-return work. Machine client-tool-result follow-ups are explicitly refused pending FE-1438 (the client-tool round-trip). | The agent cannot yet apply a projection to the live document and receive the result. | +| Session target | The current application derives `targetDocumentId` from `conversationId`. | A new reviewer session cannot address a pre-existing elicitation target without changing this identity boundary. | +| Demo website | The production website still uses its stock assistant route. The `/brunch` Actual Mode is a separate read-only fixture/SSE surface. | Local tracer proof must not be mistaken for deployed integration. | + +The decisive reading is that the current design is not too rigorous in its preservation of +evidence, correction, or register boundaries. It is too broad and too generic for the remaining +time. Completing generic plugin machinery, a second target, a full CPS ontology, and a cold-start +interviewer before crossing the real reviewer loop would optimize the library while leaving the +demo hollow. + +## The strategic bet + +Build the smallest honest **CPS review-and-revise loop** through all three registers and the real +Petrinaut entrypoint. Let that concrete implementation discover the minimum plugin interface, then +generalize only what the CPS case and existing Gherkin case both need. + +This is not permission to take another thin tracer as the definition of done. The vertical proof is +contract-bearing: it includes model assembly, provenance, targeted correction, reprojection, +application, and the deployed route. Breadth inside each layer may be narrow; no layer in that loop +may be a fixture masquerading as production wiring. + +The bet preserves these load-bearing decisions: + +- Captures remain the durable, source-grounded assertion register. +- Every semantic inference happens at write time and is recorded as a contestable capture. +- The elicited model is a pure fold over active captures and every model part names its supporting + capture IDs. +- SDCPN projection consumes the elicited model without rereading the transcript or making hidden + semantic judgments. +- Petrinaut application and diagnostics are separate from semantic projection: the application may + use client tools to apply a projected artifact, but it does not become the authority that invents + the model. +- A correction supersedes or adds assertions and re-runs the fold and projection; it does not patch + an unexplained net element directly. + +FE-1480 (requirements-model-to-SDCPN inference) challenges the third and fourth decisions by +assuming the projection itself requires LLM inference. That assumption is unresolved. If a worked +CPS case proves that the register-2 model is insufficient for pure projection, the honest choices +are to record the missing semantic judgment as a capture before folding or to amend ADR-0003 (the +three-register IR) explicitly. Hiding inference inside a read-time projection is not an available +shortcut. + +## The elicitor architecture under this load + +The discussion began with four parts; the current model has five responsibilities across the +harness and plugin layers, plus one per-engagement input. The missing responsibility is the +controller that closes the loop between captured evidence and the next move. + +| Responsibility | Owner | What it contains | State and September obligation | +| --- | --- | --- | --- | +| Strategy repertoire | Harness | Orientations, motivations, conversational licences, interviewing techniques, and question-formulation guidance. | Partly researched, not operationally selected. Implement only the techniques used by the review-and-revise runbook. | +| Evidence engine | Harness | Archive, settlement sweep, quote anchoring, durable captures, issues, conflict, supersession, and provenance primitives. | Strongest implemented layer. Add the active-model/issues read path needed by control and correction; do not broaden storage semantics without evidence. | +| Elicitation controller | Harness | Reads the engagement brief, active folded model and issues, current runbook, and strategy repertoire; chooses `ask`, `propose`, `contrast`, `validate`, `project`, `explain`, or `stop`. | Absent. Build the narrow controller loop needed to explain and revise one selected region. | +| Domain contract | Plugin | Proposal and model schemas; identity, fold, grade, demand, diagnostics, projection, and provenance rules for one target domain. | Designed but unimplemented. Build the CPS subset exercised by the fixture and correction; let it pressure the generic interface. | +| Job runbooks | Plugin | Named jobs over the same domain: objectives, entry conditions, trajectories, demand/completion rules, checks, stopping, revision, boundaries, and handoff. | Absent. Implement `review-and-revise`; defer a complete cold-start runbook. | + +The **engagement brief** is dynamic input, not plugin policy: target document, participant role, +objective, scope, known constraints, allowed actions, and time budget for this run. For September it +binds a reviewer to an existing target and one revisable region. + +A separate free-form “next-question ledger” should not become another authority. Most of it is a +derived control trace: + +```text +runbook demand -> model gap or issue -> candidate move -> chosen move -> concrete ask +``` + +Persist only what replay, audit, or explicit user commitment requires. The controller must be able +to explain its chosen move from the runbook and active model; it must not accumulate an independent +shadow plan. + +The September `review-and-revise` runbook is provisionally: + +```text +entry: + existing target + folded requirements model + projected net + reviewer scope +trajectory: + orient -> select -> explain provenance -> frame correction + -> ask/validate (3-5 turns) -> show semantic and net delta -> confirm -> hand off +done: + scoped demands are met at the declared grade + no open conflict blocks the selected projection + reviewer confirms the intended delta + every changed net element retains provenance +boundary: + do not expand into cold-start elicitation or unrelated net repair +``` + +## Proof frontiers and execution order + +The work has four frontiers. They are ordered by learning dependency, not by which ticket is +currently unblocked. The semantic and experience lanes start in parallel after Frontier 0, then +join as early as possible; they are not two long independent streams to integrate at the end. + +### Frontier 0 — make the demo claim decidable + +Confirm the business use case, freeze one representative prebuilt requirements-model/net fixture, +and name the optimisation handoff artifact. On that fixture, settle the FE-1480 authority question: +which steps are write-time semantic capture, pure model fold, pure SDCPN projection, and document +application? + +The prebuilt fixture must be a valid register-1 store state with a source conversation and +quote-anchored captures produced through, or independently validated against, the production +capture/fold path. A hand-authored register-2 model or register-3 net cannot prove provenance and +cannot serve as the correction baseline. + +**Proof:** one reviewed worked transformation in which every SDCPN element needed by the scenario +traces to model fields and captures, with every non-mechanical judgment assigned to a write-time +producer. If this cannot be drawn honestly, implementation should not freeze an interface. + +### Frontier 1 — close the CPS semantic loop + +Implement only the CPS proposal kinds, model slots, identity/fold rules, demands, projection, and +provenance exercised by the fixture and one realistic correction. Carry capture IDs through every +derived layer. Make active model issues and selected-region context available to the controller. + +**Proof:** from the production fold/projection APIs, one source-grounded supersession changes the +expected model field and corresponding SDCPN elements, leaves an unrelated region stable, and +answers both forward and reverse provenance queries. A YAML or Markdown rendering of the model is +enough for inspection at this frontier. The proving proposal must have the shape the production +sweep will emit; the cross-frontier join is not accepted until that sweep produces it from the +reviewer's actual utterance rather than a test inserting it directly. + +### Frontier 2 — close the reviewer control loop + +Allow a new conversation to bind to an existing target document. Admit the machine client-tool +results needed to apply and diagnose a net change. Mount the narrow `review-and-revise` runbook and +controller so that the active model and selected region, rather than the raw transcript alone, +drive three to five questions. + +Session binding and client-tool admission may proceed in parallel with Frontier 1. Mounting the +controller against active model/issues waits for Frontier 1's production read path; do not replace +that dependency with request-shaped model context. + +**Proof:** through the real Brunch HTTP handler and Petrinaut panel, a reviewer selects the prepared +region, receives a grounded explanation, submits a scoped correction, and sees the returned apply +result resume the same durable session. The net delta must trace to a superseding capture produced +by the production sweep from the reviewer's utterance, not one inserted by the test or fabricated +by the controller. No test-only injection supplies the target or tool wiring. + +### Frontier 3 — converge on the deployed demo + +Wire provider/mode routing, browser principal and private session lookup, remote transport, +deployment gates, and the optimisation handoff. Rehearse the exact scenario with a clean browser +against the deployed demo surface. + +**Proof:** a screen-recordable run completes the six September beats, survives one reload, exposes +the before/after requirements-model delta, and hands the resulting SDCPN to the optimisation flow. +Diagnostics show the source capture and projection identities needed to investigate a failure. + +## What is deliberately cut + +Until the proof spine is closed: + +- Do not freeze a broad declarative plugin SDK or require a second hard target. Extract the shared + contract after CPS has stressed it. +- Do not make the Gherkin artifact path a prerequisite for the CPS demo. +- Do not build a full requirements-graph UI. FE-1481's YAML or Markdown export is the selected + fallback; a UI earns time only if the core loop is already green. +- Do not build a complete cold-start CPS interview, general target gallery, every affordance type, + voice input, surprising-scenario generation, or broad telemetry vocabulary. +- Do not implement a comprehensive CPS ontology. Support the fixture, the correction, and the + optimisation handoff while keeping the data model honest about what it omits. +- Do not bypass provenance or write-time semantics to make a visually convincing net mutation. + +These are sequencing cuts, not claims that the deferred obligations are unimportant. + +## Issue projection + +The PM-authored issues are adopted here as the September delivery decomposition. Linear has not yet +been changed; its current unparented state is recorded in COORDINATION until an explicitly approved +registry update. The recommended hierarchy is FE-1357 (September planning and plugin design) → +FE-1476 (September delivery) → FE-1477 through FE-1482. + +| Issue | Strategic role | Reconciliation with existing work | +| --- | --- | --- | +| FE-1476 — prepare the September demo | Outcome owner and acceptance narrative. | Child of FE-1357 while that map remains active; owns rehearsal and handoff rather than implementation details. | +| FE-1477 — route Petrinaut AI and Brunch | Experience-lane entry and mode selection. | Product acceptance overlaps FE-1440 (ship the elicitor in the demo site). Keep one implementation owner; do not build two switches. | +| FE-1478 — trace a generated net to requirements | Provenance acceptance through registers 3 → 2 → 1 → utterance. | Must shape Frontier 1 from its first model/projection types, not arrive as post-hoc metadata. | +| FE-1479 — targeted re-elicitation | Convergence issue for the reviewer loop. | Consumes FE-1438's machine client-tool/application path, FE-1439's session ownership, and the CPS correction path; it does not own a second mutation mechanism. | +| FE-1480 — infer requirements model to SDCPN | Authority and projection decision, then the production projector. | Must be reconciled with ADR-0003 before implementation. FE-1438 owns browser application, not hidden semantic projection. | +| FE-1481 — expose the requirements model | Inspection fallback and demo delta surface. | Select YAML/Markdown first. Defer FE-1442's broader live capture/completion UI unless the proof spine closes early. | +| FE-1482 — add the CPS plugin | Semantic-lane owner and concrete pressure on the plugin boundary. | Pulls the demo-critical slices from FE-1402 (completion), FE-1403 (CPS guidance), FE-1406 (strategies), and FE-1431 (declarative contract). FE-1393 remains the generic/Gherkin path and no longer gates September. | + +Other consequences for the old graph: + +- FE-1387 (second target and plugin-contract freeze) follows the CPS proof instead of preceding the + demo. +- FE-1331 (start from create-new-net) is outside the current reviewer-against-existing-target + scenario, but ADR-0004 explicitly un-deferred it as September topology. The FE-1476 scenario + therefore creates an exposed conflict pending Dora's confirmation and, if review-and-revise + stands, a dated ADR-0004 amendment; this steering document does not silently re-defer it. +- FE-1438, FE-1439, FE-1440, FE-1423 (pre-remote gates), and FE-1441 (deployment) remain real + implementation obligations; the new issues state user outcomes rather than replacing these + substrate and release seams. +- FE-1402, FE-1403, FE-1406, and FE-1431 should produce only what the CPS runbook and domain + contract consume. Their old standalone completion must not become a hidden prerequisite. +- The active Petrinaut integration spec still describes a cold-start interview in some user + stories. Reconcile those stories with the confirmed scenario rather than treating this plan as a + silent specification amendment. + +## Beliefs, risks, and replan conditions + +| Current belief | Confidence and evidence | Replan when | +| --- | --- | --- | +| A bounded review-and-revise scenario can carry the September product claim without cold-start elicitation. | Medium. It is the written FE-1476 scenario, but Dora has not confirmed the use case. | The confirmed use case requires model creation rather than review, or the optimisation handoff requires fields absent from the fixture. | +| A concrete CPS implementation will discover a better minimum plugin contract faster than completing the generic design first. | Medium-high. Gherkin deliberately under-stresses the interface; CPS is the first real consumer. | The first worked CPS transformation cannot be expressed without a reusable harness primitive that must precede it. Build that primitive, then return immediately to the vertical proof. | +| Register 2 can be rich enough for pure SDCPN projection. | Low-medium. ADR-0003 requires it, but no real fold or projector exists and FE-1480 asserts non-determinism. | The worked transformation identifies an unavoidable semantic choice not represented in captures/model. Record it earlier or explicitly revisit the ADR. | +| Three to five turns can produce a meaningful scoped correction. | Low. No CPS runbook has been rehearsed. | Two rehearsals exceed the budget or require unrelated context. Narrow the region, preload explicit context, or revise the demo claim rather than script fake success. | +| The proven ask suspension can extend to document-application client tools. | Medium-low. The suspension spike succeeded, but the production transport intentionally refuses machine tool-result follow-ups. | The first tool round-trip cannot preserve correlation, durability, or non-user evidence semantics. Treat this as a critical integration blocker, not a UI detail. | +| The production website and remote server can be joined inside the timebox. | Medium-low. Local host seams exist; provider routing, session ownership, deployment, and release gates do not yet converge. | A deployable path is not proved by the end of Frontier 2. Preserve the real semantic loop and seek an explicit demo-surface decision rather than quietly falling back to test-only wiring. | +| Requirements-model UI is unnecessary for comprehension. | Medium. The reviewer needs inspectability, but FE-1481 explicitly permits export. | Rehearsal shows that provenance and delta cannot be understood from chat plus a structured export. | + +## Current choice + +Stop treating “unblocked” as “next.” The next strategic move is to run Frontier 0 immediately and +open Frontiers 1 and 2 in parallel: one worked CPS semantic slice and one existing-target reviewer +session/tool-return slice. Join them at the earliest correction, then drive the same bones through +provider routing and deployment. The generic Gherkin/plugin-freeze path and broad UI work wait for +that join. + +Revisit this choice as soon as Dora confirms the use case, the FE-1480 worked transformation lands, +or either parallel frontier fails its first proof. A steering pass that only updates ticket status +does not change this document; a new fact that changes the objective, proof spine, authority +boundary, or cut line does. From 9f4a228fb9384a83d2527777db894ef4ea0e12cd Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 13:48:41 +0200 Subject: [PATCH 02/11] Register incoming SDCPN nets Register seven incoming Petrinaut SDCPN layout snapshots in the documentation inbox and index them as unsettled corpus material. Preserve the source artifacts unchanged while making their provenance and intended frontier consumer explicit. Amp-Thread-ID: https://ampcode.com/threads/T-01a032f5-37ff-7328-a0d5-f8ee48678dd2 Co-authored-by: Amp --- libs/@hashintel/brunch-agent/docs/INDEX.md | 4 +- ...gases-1-pn-consumption-trigger-layout.json | 486 ++++ .../docs/inbox/SDCPNs/gases-1-pn-layout.json | 472 ++++ .../docs/inbox/SDCPNs/gases-2-spn-layout.json | 978 ++++++++ .../docs/inbox/SDCPNs/gases-3-cpn-layout.json | 1300 ++++++++++ .../inbox/SDCPNs/gases-4-dcpn-layout.json | 1619 ++++++++++++ .../semiconductor-fab-drift-layout.json | 1509 +++++++++++ ...k-fleet-predictive-maintenance-layout.json | 2235 +++++++++++++++++ 8 files changed, 8602 insertions(+), 1 deletion(-) create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-consumption-trigger-layout.json create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-1-pn-layout.json create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-2-spn-layout.json create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-3-cpn-layout.json create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/gases-4-dcpn-layout.json create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/semiconductor-fab-drift-layout.json create mode 100644 libs/@hashintel/brunch-agent/docs/inbox/SDCPNs/truck-fleet-predictive-maintenance-layout.json diff --git a/libs/@hashintel/brunch-agent/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index b0834155a7d..174504cc176 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -8,7 +8,9 @@ copy lives outside the repo). ## 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 JSON files: two plain-net gas variants, gas SPN/CPN/DCPN models, truck-fleet maintenance, and semiconductor-fab drift; the expected fifth gas SDCPN is not present in this arrival | Frontier 0 corpus inventory; settlement/conformance pending | ## Reference (settled sources) 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..16be4b70b64 --- /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)" +} \ No newline at end of file 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..c5fa9b7e740 --- /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" +} \ No newline at end of file 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..9e69eac225e --- /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" +} \ No newline at end of file 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..1bb69ebe485 --- /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" +} \ No newline at end of file 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..3f8a50e3d5d --- /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" +} \ No newline at end of file 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)" +} From 58cc514e08a583f63570805f1cc6a137fc663113 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 13:53:19 +0200 Subject: [PATCH 03/11] Correct SDCPN corpus inventory Correct the SDCPN index digest to treat the stale gases-5 reference as unconfirmed rather than a missing arrival. Keep the corpus count at seven and state the actual gas, truck, and semiconductor contents. Amp-Thread-ID: https://ampcode.com/threads/T-01a032f5-37ff-7328-a0d5-f8ee48678dd2 Co-authored-by: Amp --- libs/@hashintel/brunch-agent/docs/INDEX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@hashintel/brunch-agent/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index 174504cc176..15211fbdcf6 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -10,7 +10,7 @@ copy lives outside the repo). | Document | Status | Date | Digest | Used by | | ---------------------------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | -| [SDCPN JSON nets](inbox/SDCPNs/) | inbox | 2026-08-24 | Seven valid Petrinaut v1 JSON files: two plain-net gas variants, gas SPN/CPN/DCPN models, truck-fleet maintenance, and semiconductor-fab drift; the expected fifth gas SDCPN is not present in this arrival | Frontier 0 corpus inventory; settlement/conformance pending | +| [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) From 2f3f795a1914bab7ca927a9dc18d162d9fe3ab5f Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 14:11:45 +0200 Subject: [PATCH 04/11] Add strategic steering protocol Define a trigger-driven loop that classifies inputs, chooses a falsifiable proof frontier, requires production-path evidence, and deposits each changed truth in one authority. Route Brunch guidance, arc close, and issue tracking through that loop so mechanical issue availability does not substitute for strategy. --- libs/@hashintel/brunch-agent/AGENTS.md | 36 ++-- libs/@hashintel/brunch-agent/docs/INDEX.md | 13 +- .../brunch-agent/docs/agents/arc-close.md | 25 ++- .../brunch-agent/docs/agents/documentation.md | 161 ++++++++++-------- .../brunch-agent/docs/agents/issue-tracker.md | 22 ++- .../brunch-agent/docs/agents/steering.md | 103 +++++++++++ 6 files changed, 250 insertions(+), 110 deletions(-) create mode 100644 libs/@hashintel/brunch-agent/docs/agents/steering.md diff --git a/libs/@hashintel/brunch-agent/AGENTS.md b/libs/@hashintel/brunch-agent/AGENTS.md index f38e3481753..c8ffdc6ce59 100644 --- a/libs/@hashintel/brunch-agent/AGENTS.md +++ b/libs/@hashintel/brunch-agent/AGENTS.md @@ -25,24 +25,18 @@ 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` + +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/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index 15211fbdcf6..94284de683b 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -6,6 +6,11 @@ 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 existing +`docs/planning/` and `docs/history/` sections below describe legacy physical paths pending an +atomic pointer-safe migration; add no new files there. Agent protocols are registered through +`AGENTS.md`; the strategic control loop is [`docs/agents/steering.md`](agents/steering.md). + ## Inbox (awaiting settlement) | Document | Status | Date | Digest | Used by | @@ -25,7 +30,7 @@ copy lives outside the repo). | [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) +## Legacy history/planning/elicitation-kernel (effort complete 2026-08-10; settled 2026-08-12) | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -36,7 +41,7 @@ copy lives outside the repo). | [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) | -## planning/process-model-elicitation (effort active — FE-1357) +## Legacy planning/process-model-elicitation (effort active — FE-1357) | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -66,7 +71,7 @@ copy lives outside the repo). | [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) +## Legacy planning/\_shared (cross-effort control documents) | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -77,7 +82,7 @@ copy lives outside the repo). | [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 | -## planning/legibility-sweep (FE-1401 arc records) +## Legacy planning/legibility-sweep (FE-1401 arc records) | Document | Status | Linear | Digest | | ------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md index 5f24239ee15..089e36df47b 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md +++ b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md @@ -18,11 +18,12 @@ 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 + legacy `docs/planning/` or `docs/history/` paths; their existing contents await atomic migration. - 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 @@ -33,10 +34,12 @@ 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 +- 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 `COORDINATION.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`. @@ -62,7 +65,14 @@ state, and hierarchy. Keep one compact pseudo-style map of the current recommend the full generated graph or maintain alternative or historical orderings. If the judgment did not change, leave the document untouched. -### 5. Repair tense and report +### 5. Reconcile steering and proof when triggered + +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 @@ -78,4 +88,5 @@ Arc close is complete when: 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. +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..413da94e5ab 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/` | Compact, mutable strategic, coordination, and obligation control surfaces | +| `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. + +`docs/planning/` and `docs/history/` are legacy paths pending one atomic migration. Existing files +remain authoritative according to their present role until that migration repairs all external +pointers and updates the index. Add no new files to either path, and do not describe the target +topology as already physically complete. Do not create `evaluations/` until executable assets are +actually promoted there. + +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 + +Controls under `docs/control/` carry only current objective, topology, obligations, choices, gates, +and stop conditions. Keep them compact. Link immutable evidence and historical context instead of +copying it or appending chronology; Git records control-surface history. Linear owns issue state, +hierarchy, and hard blockers, while repo controls may project those facts and record soft edges. + +## 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/issue-tracker.md b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md index 60d329f0c94..b33398d6b14 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,6 +12,10 @@ agent authors from this repo. ## Conventions +**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`. Create issues with `linear issue create --team FE --project brunch-agent`. The project is the ownership boundary for this codebase — see the registry rule below. @@ -26,8 +31,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 +73,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. `COORDINATION` projects that topology and adds soft edges; `STEERING` 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,8 +86,8 @@ 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. +- **Claimable queue / mechanical frontier**: open, unblocked, **unassigned** sub-issues of the map + — lowest issue number first. This is an availability filter, not the strategic proof frontier. - **Claim**: assign the issue to yourself (the dev driving the map) before any work. - **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). 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..fa6a3f2ca74 --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/agents/steering.md @@ -0,0 +1,103 @@ +# 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 mutable strategy itself lives in `STEERING`; this document owns +only the recurring 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 claimable queue (the mechanically open, unblocked, unassigned 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, strategic choice, cuts, beliefs, and gates | `STEERING` | +| Issue-graph projection and soft edges | `COORDINATION` | +| 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. + +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. From 5d01a779840868f5b81a6afb8de4d1325d9f8bb9 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 14:42:33 +0200 Subject: [PATCH 05/11] precedent doc refinements Record shared-worktree Graphite safety, audience-calibrated issue prose, and the rule for promoting recurrent point findings into executable audits. Correct the import plan's stale gh-stack reference so its migration guidance matches the accepted Graphite workflow. Signed-off-by: Lu Nelson --- .../brunch-agent/docs/agents/git-workflow.md | 8 ++++++++ .../brunch-agent/docs/agents/issue-writing.md | 6 ++++++ .../brunch-agent/docs/agents/legibility.md | 12 ++++++++++++ .../planning/_shared/hash-monorepo-import-plan.md | 2 +- 4 files changed, 27 insertions(+), 1 deletion(-) 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-writing.md b/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md index 8f24631c13c..30d6bc132d8 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 diff --git a/libs/@hashintel/brunch-agent/docs/agents/legibility.md b/libs/@hashintel/brunch-agent/docs/agents/legibility.md index 20d5a462492..49cca99c50f 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 diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/hash-monorepo-import-plan.md b/libs/@hashintel/brunch-agent/docs/planning/_shared/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/planning/_shared/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. | From 8c53c05a6a2e7f11b080546a77ee5b40b057e7f3 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 14:57:41 +0200 Subject: [PATCH 06/11] Organize Brunch documentation by role Move Brunch documents from effort-shaped planning and history trees into control, specification, evidence, reference, and archive authorities. Update links, the index, and architecture gates atomically so no live document remains under the retired paths. --- libs/@hashintel/brunch-agent/CONTEXT.md | 4 +- libs/@hashintel/brunch-agent/README.md | 2 +- libs/@hashintel/brunch-agent/docs/INDEX.md | 105 +++++++++--------- .../adr/0001-brunch-is-the-product-name.md | 2 +- .../adr/0002-topology-and-placement-rules.md | 8 +- .../docs/adr/0003-three-register-ir.md | 2 +- ...trinaut-staging-and-the-monorepo-import.md | 4 +- .../brunch-agent/docs/agents/arc-close.md | 8 +- .../brunch-agent/docs/agents/documentation.md | 9 +- .../brunch-agent/docs/agents/flue-routing.md | 4 +- .../brunch-agent/docs/agents/issue-tracker.md | 7 +- .../brunch-agent/docs/agents/issue-writing.md | 2 +- .../recommendation-demo-vehicle.md | 0 .../issues/01-flue-architecture-deep-read.md | 0 .../issues/02-zil-lean-survey.md | 0 .../issues/03-brunch-exchange-schema-audit.md | 0 .../issues/04-contract-decomposition.md | 2 +- .../issues/05-questioning-ux-contract.md | 0 .../issues/06-shipping-shape.md | 0 .../issues/07-dev-target-portfolio.md | 0 .../issues/08-assemble-the-spec.md | 2 +- .../09-formal-verification-canon-survey.md | 0 .../10-walking-skeleton-flue-roundtrip.md | 0 .../11-logic-prototype-capture-sweep.md | 0 .../issues/12-multi-session-durable-target.md | 0 .../issues/13-walking-skeleton-sweep-seam.md | 0 .../elicitation-kernel/map.md | 6 +- .../notes/consistency-prepass-2026-08-10.md | 0 .../product-description-plain.md | 2 +- .../elicitation-kernel/product-description.md | 2 +- .../legibility}/refactor-queue-2026-08-14.md | 0 .../remediation-plan-2026-08-17.md | 4 +- .../review-remediation-2026-08-18.md | 0 ...questions-elicitation-design-2026-08-11.md | 0 .../expert-meeting-prep-2026-08-11.md | 0 .../migrations}/hash-monorepo-import-plan.md | 0 .../issue-pr-legibility-2026-08-20}/README.md | 0 .../apply-log.jsonl | 0 .../data/github-proposals.json | 0 .../data/github-source.json | 0 .../data/linear-canonical-target-hashes.json | 0 .../data/linear-proposals-FE-1366.json | 0 .../data/linear-proposals-FE-1383.json | 0 .../data/linear-proposals-FE-1401.json | 0 .../data/linear-proposals-FE-1406.json | 0 .../data/linear-source.json | 0 .../data/source-record-hashes.json | 0 .../review/github-editorial-review.md | 0 .../review/github.md | 0 .../review/linear-FE-1357.md | 0 .../review/linear-FE-1366.md | 0 .../review/linear-FE-1383.md | 0 .../review/linear-FE-1401.md | 0 .../review/linear-FE-1406.md | 0 .../review/linear-editorial-review.md | 0 .../validate.ts | 0 .../grilling-inputs-2026-08-12.md | 2 +- .../penciled-directions-2026-08-14.md | 0 .../_shared => control}/COORDINATION.md | 4 +- .../_shared => control}/SPEC-LEDGER.md | 0 .../{planning/_shared => control}/STEERING.md | 2 +- .../proofs/audits}/deep-read-fe-1389.md | 2 +- .../proofs/audits}/deep-read-fe-1390.md | 3 +- ...entry-projection-source-read-2026-08-18.md | 0 .../audits}/flue-patterns-audit-2026-08-17.md | 0 .../proofs/audits}/research-patterns-audit.md | 2 +- ...mediate-representation-worked-examples.md} | 2 +- .../fe-1436-transport-aisdk-2026-08-19.md} | 0 .../fe-1449-ask-return-2026-08-19.md} | 0 .../expert-meeting-findings-2026-08-11.md | 2 +- ...e-1434-suspension-evidence-2026-08-19.json | 0 .../fe-1434-suspension-verdict-2026-08-19.md | 4 +- .../fe-1435-adapter-panel-2026-08-19.md} | 0 .../brunch-agent/docs/inbox/.gitkeep | 0 .../baseline/protocol.md | 2 +- .../architecture/capture-store.md} | 0 .../flue-architecture-cheatsheet.md | 4 +- .../architecture}/topology.md | 4 +- .../elicitation-strategy-literature.md | 4 +- ...interviewing-literature-source-catalog.md} | 0 .../research/petrinaut-survey.md | 0 .../research/voice-feasibility.md | 16 +-- .../{spec.md => specs/elicitation-kernel.md} | 10 +- .../intermediate-representation-plain.md} | 6 +- .../intermediate-representation.md} | 6 +- .../petrinaut-integration.md} | 0 .../plugin-contract.md} | 4 +- .../core/test/architecture/boundaries.test.ts | 5 +- .../core/test/architecture/docs-index.test.ts | 37 +++--- 89 files changed, 156 insertions(+), 140 deletions(-) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation => archive/decisions/superseded}/recommendation-demo-vehicle.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/01-flue-architecture-deep-read.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/02-zil-lean-survey.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/03-brunch-exchange-schema-audit.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/04-contract-decomposition.md (99%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/05-questioning-ux-contract.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/06-shipping-shape.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/07-dev-target-portfolio.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/08-assemble-the-spec.md (91%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/09-formal-verification-canon-survey.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/10-walking-skeleton-flue-roundtrip.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/11-logic-prototype-capture-sweep.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/12-multi-session-durable-target.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/issues/13-walking-skeleton-sweep-seam.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/map.md (93%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/notes/consistency-prepass-2026-08-10.md (100%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/product-description-plain.md (98%) rename libs/@hashintel/brunch-agent/docs/{history/planning => archive}/elicitation-kernel/product-description.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep => archive/engineering/legibility}/refactor-queue-2026-08-14.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep => archive/engineering/legibility}/remediation-plan-2026-08-17.md (96%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep => archive/engineering/legibility}/review-remediation-2026-08-18.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => archive/external-snapshots}/open-questions-elicitation-design-2026-08-11.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => archive/meetings}/expert-meeting-prep-2026-08-11.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/_shared => archive/migrations}/hash-monorepo-import-plan.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/README.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/apply-log.jsonl (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/github-proposals.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/github-source.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/linear-canonical-target-hashes.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/linear-proposals-FE-1366.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/linear-proposals-FE-1383.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/linear-proposals-FE-1401.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/linear-proposals-FE-1406.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/linear-source.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/data/source-record-hashes.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/github-editorial-review.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/github.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/linear-FE-1357.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/linear-FE-1366.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/linear-FE-1383.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/linear-FE-1401.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/linear-FE-1406.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/review/linear-editorial-review.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep/issue-pr-migration-2026-08-20 => archive/migrations/issue-pr-legibility-2026-08-20}/validate.ts (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => archive/planning-inputs}/grilling-inputs-2026-08-12.md (98%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => archive/planning-inputs}/penciled-directions-2026-08-14.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/_shared => control}/COORDINATION.md (97%) rename libs/@hashintel/brunch-agent/docs/{planning/_shared => control}/SPEC-LEDGER.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/_shared => control}/STEERING.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => evidence/proofs/audits}/deep-read-fe-1389.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => evidence/proofs/audits}/deep-read-fe-1390.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep => evidence/proofs/audits}/flue-entry-projection-source-read-2026-08-18.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/legibility-sweep => evidence/proofs/audits}/flue-patterns-audit-2026-08-17.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => evidence/proofs/audits}/research-patterns-audit.md (98%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/ir-worked-examples.md => evidence/proofs/design/intermediate-representation-worked-examples.md} (99%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/transport-aisdk-implementation-2026-08-19.md => evidence/proofs/implementations/fe-1436-transport-aisdk-2026-08-19.md} (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/ask-return-implementation-2026-08-19.md => evidence/proofs/implementations/fe-1449-ask-return-2026-08-19.md} (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/notes => evidence/proofs/research}/expert-meeting-findings-2026-08-11.md (97%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation => evidence/proofs}/spikes/fe-1434-suspension-evidence-2026-08-19.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation => evidence/proofs}/spikes/fe-1434-suspension-verdict-2026-08-19.md (97%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/adapter-panel-spike-2026-08-19.md => evidence/proofs/spikes/fe-1435-adapter-panel-2026-08-19.md} (100%) delete mode 100644 libs/@hashintel/brunch-agent/docs/inbox/.gitkeep rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/capture-store-plain.md => reference/architecture/capture-store.md} (100%) rename libs/@hashintel/brunch-agent/docs/{planning/_shared => reference/architecture}/flue-architecture-cheatsheet.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning/_shared => reference/architecture}/topology.md (97%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/research => reference/research/elicitation}/elicitation-strategy-literature.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/research/re-interviewing-literature-worker-report.md => reference/research/elicitation/interviewing-literature-source-catalog.md} (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation => reference}/research/petrinaut-survey.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation => reference}/research/voice-feasibility.md (98%) rename libs/@hashintel/brunch-agent/docs/{spec.md => specs/elicitation-kernel.md} (99%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/ir-design-plain.md => specs/intermediate-representation-plain.md} (97%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/ir-design.md => specs/intermediate-representation.md} (98%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/petrinaut-integration-spec.md => specs/petrinaut-integration.md} (100%) rename libs/@hashintel/brunch-agent/docs/{planning/process-model-elicitation/plugin-contract-spec.md => specs/plugin-contract.md} (99%) 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..3c7612aa030 100644 --- a/libs/@hashintel/brunch-agent/README.md +++ b/libs/@hashintel/brunch-agent/README.md @@ -6,7 +6,7 @@ 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. - [`packages/core/`](./packages/core/) is `@hashintel/brunch-agent`. - [`packages/binding-flue/`](./packages/binding-flue/) is the Flue binding. diff --git a/libs/@hashintel/brunch-agent/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index 94284de683b..e7cf7cc2d05 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -6,9 +6,8 @@ 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 existing -`docs/planning/` and `docs/history/` sections below describe legacy physical paths pending an -atomic pointer-safe migration; add no new files there. Agent protocols are registered through +The authoritative role topology is defined by the documentation protocol. The role-based zones below are authoritative. Only the baseline experiment remains temporarily +under `docs/planning/`; `docs/history/` is retired. Agent protocols are registered through `AGENTS.md`; the strategic control loop is [`docs/agents/steering.md`](agents/steering.md). ## Inbox (awaiting settlement) @@ -30,68 +29,68 @@ atomic pointer-safe migration; add no new files there. Agent protocols are regis | [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 | -## Legacy 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) | -## Legacy 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 | +| [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/](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 | - -## Legacy planning/\_shared (cross-effort control documents) +| [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 | -| [STEERING](planning/_shared/STEERING.md) | active | FE-1357 / FE-1476 | Current objective-driven strategy for the September CPS review-and-revise proof: evidence, architectural pressure, proof frontiers, cuts, issue projection, risks, and replan conditions | -| [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 | +| [COORDINATION](control/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](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 | Current objective-driven strategy for the September CPS review-and-revise proof: evidence, architectural pressure, proof frontiers, cuts, issue projection, risks, and replan conditions | +| [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 | -## Legacy 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/`) @@ -118,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 089e36df47b..baf83cdbd2c 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md +++ b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md @@ -19,7 +19,7 @@ the history of these control surfaces. Always inspect `docs/inbox/` and `docs/INDEX.md` together. - Promote settled inbox material to its role-based home under `documentation.md`. Do not add to the - legacy `docs/planning/` or `docs/history/` paths; their existing contents await atomic migration. + legacy `docs/planning/` or `docs/history/` paths; only the transitional baseline remains. - Delete source material only when its durable information has been transferred and its consumption is named. - Ensure every document under `docs/` except `docs/INDEX.md` and `docs/agents/**` is indexed and @@ -33,7 +33,7 @@ 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`. + named under **Exceptional roots** in `docs/control/COORDINATION.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 `COORDINATION.md`. @@ -45,14 +45,14 @@ issue with no parent. ### 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 -Reassess `docs/planning/_shared/COORDINATION.md` when the arc changes: +Reassess `docs/control/COORDINATION.md` when the arc changes: - a hard blocker, issue parent, project membership, or exceptional root; - a soft `coord`, `input`, or `state-gate` edge; diff --git a/libs/@hashintel/brunch-agent/docs/agents/documentation.md b/libs/@hashintel/brunch-agent/docs/agents/documentation.md index 413da94e5ab..dd58be6d459 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/documentation.md +++ b/libs/@hashintel/brunch-agent/docs/agents/documentation.md @@ -23,11 +23,10 @@ and `docs/INDEX.md` remains the registry described below. Effort is metadata recorded in the index or document, not a placement rule. Do not create an effort-shaped documentation tree. -`docs/planning/` and `docs/history/` are legacy paths pending one atomic migration. Existing files -remain authoritative according to their present role until that migration repairs all external -pointers and updates the index. Add no new files to either path, and do not describe the target -topology as already physically complete. Do not create `evaluations/` until executable assets are -actually promoted there. +The role topology is physically authoritative. During the baseline follow-up only, +`docs/planning/process-model-elicitation/baseline/**` remains at its legacy path; no other document +may remain under `docs/planning/`, and `docs/history/` must stay empty. Add no files to either +legacy path. Do not create `evaluations/` until executable assets are actually promoted there. 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; 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/issue-tracker.md b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md index b33398d6b14..7d58d0074ce 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md +++ b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md @@ -105,7 +105,7 @@ 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/COORDINATION.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. @@ -124,9 +124,8 @@ candidate orphans continuously; its only legitimate rows are the roots themselve ## 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 30d6bc132d8..c93902b9509 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md +++ b/libs/@hashintel/brunch-agent/docs/agents/issue-writing.md @@ -179,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/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 100% 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 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/COORDINATION.md b/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md similarity index 97% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md rename to libs/@hashintel/brunch-agent/docs/control/COORDINATION.md index 871a5ad3891..705d41c92cd 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/COORDINATION.md +++ b/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md @@ -3,7 +3,7 @@ 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. The -[steering model](./STEERING.md) chooses the current objective, proof frontiers, and cuts under +[steering model](STEERING.md) chooses the current objective, proof frontiers, and cuts under pressure. This file projects that strategy onto mechanically available work: the current project-wide recommendation, soft edges, unresolved seams, and exceptional roots. @@ -71,7 +71,7 @@ not a second issue database. ## Repository handoff threshold -FE-1437 (the monorepo import; [execution plan](./hash-monorepo-import-plan.md)) was the authority +FE-1437 (the monorepo import; [execution plan](../archive/migrations/hash-monorepo-import-plan.md)) was the authority cutover, not a general freeze on harness work. It was crossed on 2026-08-21: ```text 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 100% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/SPEC-LEDGER.md rename to libs/@hashintel/brunch-agent/docs/control/SPEC-LEDGER.md diff --git a/libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md b/libs/@hashintel/brunch-agent/docs/control/STEERING.md similarity index 99% rename from libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md rename to libs/@hashintel/brunch-agent/docs/control/STEERING.md index 45f5abbb530..156b2fa7473 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/_shared/STEERING.md +++ b/libs/@hashintel/brunch-agent/docs/control/STEERING.md @@ -7,7 +7,7 @@ one Linear map. This is not a roadmap, a specification, or an issue mirror. Specifications and accepted ADRs define obligations and settled architecture. Linear is canonical for issue state, hierarchy, and hard -blockers. [COORDINATION](./COORDINATION.md) projects this strategy onto the mechanically available +blockers. [COORDINATION](COORDINATION.md) projects this strategy onto the mechanically available work. This document owns the judgment between them: which objective to pursue under the current time, evidence, risk, and product pressure; what to defer; and what new evidence would change that choice. It may expose a conflict with a specification or ADR, but cannot silently override one. 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/planning/process-model-elicitation/baseline/protocol.md b/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/protocol.md index d4166947abd..4d37a9b3fd5 100644 --- a/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/protocol.md +++ b/libs/@hashintel/brunch-agent/docs/planning/process-model-elicitation/baseline/protocol.md @@ -56,7 +56,7 @@ Transcripts land in `transcripts/` and are committed as the experiment artifact. 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)), + [../research/re-interviewing-literature-worker-report.md](../../../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/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..60b93a4359d 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) +docs/planning/process-model-elicitation/baseline/run.ts EXPERIMENT (transitional legacy path) ``` ## 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 99% 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..a67b4e7bc25 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 @@ -386,7 +386,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/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/docs-index.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/docs-index.test.ts index ae42774bfa5..09498b52bd4 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,18 @@ 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. +test("legacy documentation paths contain only the transitional baseline", () => { 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([]); + const planningFiles = FILES.filter((file) => file.startsWith("planning/")); + expect(planningFiles.length).toBeGreaterThan(0); + for (const file of planningFiles) { + expect( + file.startsWith("planning/process-model-elicitation/baseline/"), + ).toBe(true); + } + + expect(existsSync(join(DOCS_ROOT, "history"))).toBe(false); + expect(readdirSync(planning)).toEqual(["process-model-elicitation"]); }); describe.skipIf(!contextRootPresent)( From f09b1f143641cceba222e996416904d301fca8d2 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 15:12:21 +0200 Subject: [PATCH 07/11] Separate evaluation assets from evidence Move executable evaluation inputs and runners into evaluations/cases and evaluations/protocols while retaining observed transcripts and readouts under documentation evidence. Add local ownership guidance and update runner, test, pruning, and index paths so the baseline remains executable in hermetic CI. Amp-Thread-ID: https://ampcode.com/threads/T-01a032f5-37ff-7328-a0d5-f8ee48678dd2 Co-authored-by: Amp --- .github/actions/prune-repository/prune.py | 1 + libs/@hashintel/brunch-agent/AGENTS.md | 1 + libs/@hashintel/brunch-agent/docs/INDEX.md | 8 +- .../brunch-agent/docs/agents/arc-close.md | 2 +- .../brunch-agent/docs/agents/documentation.md | 6 +- .../baseline/readout.md | 4 +- .../transcripts/condition-1-model.txt | 0 .../baseline/transcripts/condition-1.md | 0 .../baseline/transcripts/condition-1.raw.json | 0 .../transcripts/condition-2-model.txt | 0 .../baseline/transcripts/condition-2.md | 0 .../baseline/transcripts/condition-2.raw.json | 0 .../docs/reference/architecture/topology.md | 2 +- .../brunch-agent/evaluations/AGENTS.md | 8 ++ .../baseline/opening-message.md | 0 .../baseline/situation-pack.md | 0 .../baseline/protocol.md | 16 ++- .../process-model-elicitation/baseline/run.ts | 35 +++++- .../baseline/v0-prompt.md | 0 .../brunch-agent/packages/core/package.json | 2 +- .../test/architecture/baseline-runner.test.ts | 114 +++++++++++++++--- .../core/test/architecture/docs-index.test.ts | 18 ++- 22 files changed, 168 insertions(+), 49 deletions(-) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/readout.md (99%) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/transcripts/condition-1-model.txt (100%) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/transcripts/condition-1.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/transcripts/condition-1.raw.json (100%) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/transcripts/condition-2-model.txt (100%) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/transcripts/condition-2.md (100%) rename libs/@hashintel/brunch-agent/docs/{planning => evidence/evaluations}/process-model-elicitation/baseline/transcripts/condition-2.raw.json (100%) create mode 100644 libs/@hashintel/brunch-agent/evaluations/AGENTS.md rename libs/@hashintel/brunch-agent/{docs/planning => evaluations/cases}/process-model-elicitation/baseline/opening-message.md (100%) rename libs/@hashintel/brunch-agent/{docs/planning => evaluations/cases}/process-model-elicitation/baseline/situation-pack.md (100%) rename libs/@hashintel/brunch-agent/{docs/planning => evaluations/protocols}/process-model-elicitation/baseline/protocol.md (87%) rename libs/@hashintel/brunch-agent/{docs/planning => evaluations/protocols}/process-model-elicitation/baseline/run.ts (95%) rename libs/@hashintel/brunch-agent/{docs/planning => evaluations/protocols}/process-model-elicitation/baseline/v0-prompt.md (100%) 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/libs/@hashintel/brunch-agent/AGENTS.md b/libs/@hashintel/brunch-agent/AGENTS.md index c8ffdc6ce59..fb46bc3be82 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 diff --git a/libs/@hashintel/brunch-agent/docs/INDEX.md b/libs/@hashintel/brunch-agent/docs/INDEX.md index e7cf7cc2d05..7ae4998872b 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -6,9 +6,9 @@ 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. Only the baseline experiment remains temporarily -under `docs/planning/`; `docs/history/` is retired. Agent protocols are registered through -`AGENTS.md`; the strategic control loop is [`docs/agents/steering.md`](agents/steering.md). +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) @@ -59,7 +59,7 @@ under `docs/planning/`; `docs/history/` is retired. Agent protocols are register | [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/](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 | +| [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) | diff --git a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md index baf83cdbd2c..487f13d1ff6 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md +++ b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md @@ -19,7 +19,7 @@ the history of these control surfaces. Always inspect `docs/inbox/` and `docs/INDEX.md` together. - Promote settled inbox material to its role-based home under `documentation.md`. Do not add to the - legacy `docs/planning/` or `docs/history/` paths; only the transitional baseline remains. + 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 document under `docs/` except `docs/INDEX.md` and `docs/agents/**` is indexed and diff --git a/libs/@hashintel/brunch-agent/docs/agents/documentation.md b/libs/@hashintel/brunch-agent/docs/agents/documentation.md index dd58be6d459..48b1606deb6 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/documentation.md +++ b/libs/@hashintel/brunch-agent/docs/agents/documentation.md @@ -23,10 +23,8 @@ and `docs/INDEX.md` remains the registry described below. 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. During the baseline follow-up only, -`docs/planning/process-model-elicitation/baseline/**` remains at its legacy path; no other document -may remain under `docs/planning/`, and `docs/history/` must stay empty. Add no files to either -legacy path. Do not create `evaluations/` until executable assets are actually promoted there. +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; 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/reference/architecture/topology.md b/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md index 60b93a4359d..1bfcb660c8d 100644 --- a/libs/@hashintel/brunch-agent/docs/reference/architecture/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/process-model-elicitation/baseline/run.ts EXPERIMENT (transitional legacy path) +evaluations/protocols/process-model-elicitation/baseline/run.ts EXPERIMENT PROTOCOL ``` ## Specification — where what's next lands 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 4d37a9b3fd5..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](../../../reference/research/elicitation/interviewing-literature-source-catalog.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/docs-index.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/docs-index.test.ts index 09498b52bd4..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 @@ -219,18 +219,14 @@ test("relative links in context documentation point at existing files", () => { } }); -test("legacy documentation paths contain only the transitional baseline", () => { - const planning = join(DOCS_ROOT, "planning"); - const planningFiles = FILES.filter((file) => file.startsWith("planning/")); - expect(planningFiles.length).toBeGreaterThan(0); - for (const file of planningFiles) { - expect( - file.startsWith("planning/process-model-elicitation/baseline/"), - ).toBe(true); - } - +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); - expect(readdirSync(planning)).toEqual(["process-model-elicitation"]); }); describe.skipIf(!contextRootPresent)( From 8d271affd67d30da511e2ae413aa9821cc0cf386 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 15:33:13 +0200 Subject: [PATCH 08/11] Reconcile Brunch control surfaces Record that the import crossed its branch authority threshold and add watched external gates for use-case confirmation and the unavailable truck-fleet dossier. Preserve the semantic and reviewer join while distinguishing review readiness from landing on main. Amp-Thread-ID: https://ampcode.com/threads/T-01a032f5-37ff-7328-a0d5-f8ee48678dd2 Co-authored-by: Amp --- .../brunch-agent/docs/control/COORDINATION.md | 11 ++++++----- libs/@hashintel/brunch-agent/docs/control/STEERING.md | 7 +++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md b/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md index 705d41c92cd..365541206a3 100644 --- a/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md +++ b/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md @@ -15,15 +15,16 @@ honest recommendation. Do not paste the generated graph here or mirror issue sta ## Current sequencing recommendation As of **2026-08-24**, FE-1476 (the September demo delivery) changes the recommendation from generic -package completion to a concrete CPS review-and-revise proof. After FE-1437 (the monorepo import) -lands, open two fronts in parallel. The semantic front starts FE-1482 (the CPS plugin) against one -worked fixture and settles FE-1480's requirements-model-to-SDCPN authority boundary before it +package completion to a concrete CPS review-and-revise proof. FE-1437 crossed the authority +threshold in this branch and is Ready for review; the work remains gated until the branch reaches +`main`. Then open two fronts in parallel. The semantic front starts FE-1482 (the CPS plugin) against +one worked fixture and settles FE-1480's requirements-model-to-SDCPN authority boundary before it implements a projector; FE-1478 (net-to-requirements provenance) is part of that spine from its first types. The experience front advances FE-1438 (machine client-tool round-trip) and FE-1439 (private sessions) far enough for a new reviewer conversation to target an existing document, while FE-1477/FE-1440 share one provider-routing implementation. Join the fronts at FE-1479 -(targeted re-elicitation), then drive the same path through FE-1423's pre-exposure gates and -FE-1441 deployment. +(targeted re-elicitation), then drive the same path through FE-1423's pre-exposure gates and FE-1441 +deployment. FE-1393's generic Gherkin artifact and FE-1387's second-target contract freeze no longer gate the September proof. FE-1402, FE-1403, FE-1406, and FE-1431 supply only the completion, guidance, diff --git a/libs/@hashintel/brunch-agent/docs/control/STEERING.md b/libs/@hashintel/brunch-agent/docs/control/STEERING.md index 156b2fa7473..dec69d1e493 100644 --- a/libs/@hashintel/brunch-agent/docs/control/STEERING.md +++ b/libs/@hashintel/brunch-agent/docs/control/STEERING.md @@ -41,6 +41,13 @@ entire CPS model from a blank conversation. It does need to prove a closed seman loop. A chat transcript beside a static fixture, an unexplained net mutation, or a test that injects wiring absent from the deployed entrypoint does not satisfy the outcome. +## External gates + +| Gate | Owner and source | Watch trigger | Last checked | Consequence | +| --- | --- | --- | --- | --- | +| Confirm the business use case and whether review-and-revise supersedes create-new-net for September. | Dora; FE-1476 and the September Plan linked from `docs/INDEX.md` | Dora confirms or changes the scenario. | 2026-08-24 | Replan the proof spine and reconcile ADR-0004 if the confirmed use case requires cold-start creation. | +| Recover or replace the truck-fleet source dossier named by FE-1357. | Owner unresolved/unknown; source: FE-1357 plus historical build ticket FE-1382; FE-1357 names branch `research/truck-fleet-dossier` and `docs/reference/research/truck-fleet-source-dossier.md` | The named branch/ref becomes available, or Frontier 0 selects a different sourced case. | 2026-08-24 — branch/ref unavailable in this checkout | Treat the dossier as an external evidence gap. Do not treat its branch-only path as a missing file in the current tree or use its claims as fixture provenance. | + The proof spine is therefore: ```text From 5483f849719598295a31ae1d27d39c7d1bac9e35 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 16:10:40 +0200 Subject: [PATCH 09/11] Consolidate Brunch strategic controls Replace overlapping STEERING and COORDINATION controls with one compact current control plus an append-only strategy log, while retaining the obligation ledger separately. Add lifecycle and project-graph checks for strategy supersession, viewer-ID assignment, pagination, and all-issue audits so the new ownership rules fail mechanically. Amp-Thread-ID: https://ampcode.com/threads/T-01a032f5-37ff-7328-a0d5-f8ee48678dd2 Co-authored-by: Amp --- libs/@hashintel/brunch-agent/README.md | 2 + libs/@hashintel/brunch-agent/docs/INDEX.md | 4 +- .../brunch-agent/docs/agents/arc-close.md | 31 +- .../brunch-agent/docs/agents/documentation.md | 13 +- .../brunch-agent/docs/agents/issue-tracker.md | 27 +- .../brunch-agent/docs/agents/legibility.md | 2 +- .../brunch-agent/docs/agents/steering.md | 16 +- .../brunch-agent/docs/control/COORDINATION.md | 147 ------- .../brunch-agent/docs/control/SPEC-LEDGER.md | 4 +- .../brunch-agent/docs/control/STEERING.md | 362 ++++-------------- .../brunch-agent/docs/control/STRATEGY-LOG.md | 53 +++ .../docs/specs/plugin-contract.md | 3 +- .../architecture/control-surfaces.test.ts | 101 +++++ .../architecture/linear-project-graph.test.ts | 208 +++++++++- .../scripts/linear-project-graph.ts | 76 +++- 15 files changed, 554 insertions(+), 495 deletions(-) delete mode 100644 libs/@hashintel/brunch-agent/docs/control/COORDINATION.md create mode 100644 libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md create mode 100644 libs/@hashintel/brunch-agent/packages/core/test/architecture/control-surfaces.test.ts diff --git a/libs/@hashintel/brunch-agent/README.md b/libs/@hashintel/brunch-agent/README.md index 3c7612aa030..28708deac2a 100644 --- a/libs/@hashintel/brunch-agent/README.md +++ b/libs/@hashintel/brunch-agent/README.md @@ -8,6 +8,8 @@ This directory is its context and agent-session root, not a package workspace: - [`docs/adr/`](./docs/adr/) records governing decisions. - [`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 7ae4998872b..aca4b6325c5 100644 --- a/libs/@hashintel/brunch-agent/docs/INDEX.md +++ b/libs/@hashintel/brunch-agent/docs/INDEX.md @@ -74,10 +74,10 @@ control loop is [`docs/agents/steering.md`](agents/steering.md). | Document | Status | Linear | Digest | | -------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [COORDINATION](control/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](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 | Current objective-driven strategy for the September CPS review-and-revise proof: evidence, architectural pressure, proof frontiers, cuts, issue projection, risks, and replan conditions | +| [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 | diff --git a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md index 487f13d1ff6..cec8e534cbc 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/arc-close.md +++ b/libs/@hashintel/brunch-agent/docs/agents/arc-close.md @@ -28,15 +28,15 @@ Always inspect `docs/inbox/` and `docs/INDEX.md` together. ### 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/control/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 `COORDINATION.md`. + 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, subject to the same approval gate for any Linear mutation. @@ -50,20 +50,17 @@ supersedes, or changes evidence for a milestone-one specification obligation. Ch 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/control/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 `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. +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. ### 5. Reconcile steering and proof when triggered @@ -87,6 +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 +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 48b1606deb6..c93786f97e5 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/documentation.md +++ b/libs/@hashintel/brunch-agent/docs/agents/documentation.md @@ -8,7 +8,7 @@ and `docs/INDEX.md` remains the registry described below. | Location | Role | | --- | --- | -| `docs/control/` | Compact, mutable strategic, coordination, and obligation control surfaces | +| `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 | @@ -70,10 +70,13 @@ accumulates run history. ## Mutable controls -Controls under `docs/control/` carry only current objective, topology, obligations, choices, gates, -and stop conditions. Keep them compact. Link immutable evidence and historical context instead of -copying it or appending chronology; Git records control-surface history. Linear owns issue state, -hierarchy, and hard blockers, while repo controls may project those facts and record soft edges. +`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 diff --git a/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md index 7d58d0074ce..757005554e7 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md +++ b/libs/@hashintel/brunch-agent/docs/agents/issue-tracker.md @@ -16,8 +16,10 @@ agent authors from this repo. 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`. Create issues with - `linear issue create --team FE --project brunch-agent`. The project is the +- **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 @@ -74,8 +76,8 @@ update. Publish it to the `brunch-agent` project. Used by the `ds-wayfind` skill. The **map** is a Linear issue with one **child** sub-issue per ticket. Linear owns issue facts: state, hierarchy, project membership, assignment, and hard -blockers. `COORDINATION` projects that topology and adds soft edges; `STEERING` chooses strategic -work by objective contribution, risk retired, and information gain. +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; @@ -86,9 +88,9 @@ work by objective contribution, risk retired, and information gain. 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). -- **Claimable queue / mechanical frontier**: open, unblocked, **unassigned** sub-issues of the map - — lowest issue number first. This is an availability filter, not the strategic proof frontier. -- **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 @@ -105,20 +107,21 @@ 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/control/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 diff --git a/libs/@hashintel/brunch-agent/docs/agents/legibility.md b/libs/@hashintel/brunch-agent/docs/agents/legibility.md index 49cca99c50f..c46aae550e4 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/legibility.md +++ b/libs/@hashintel/brunch-agent/docs/agents/legibility.md @@ -67,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 index fa6a3f2ca74..cf3faf4d19a 100644 --- a/libs/@hashintel/brunch-agent/docs/agents/steering.md +++ b/libs/@hashintel/brunch-agent/docs/agents/steering.md @@ -1,8 +1,8 @@ # 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 mutable strategy itself lives in `STEERING`; this document owns -only the recurring control loop. +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: @@ -29,8 +29,8 @@ Do not turn confidence into fact. A confidence change cites the evidence that ch ## Choose -Treat the claimable queue (the mechanically open, unblocked, unassigned issues) as a filter, not a -priority rule. Rank eligible moves by objective contribution, risk retired, information gain, +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: @@ -77,8 +77,8 @@ Deposit each changed truth into exactly one authority and link to it elsewhere: | Truth | Authority | | --- | --- | -| Current objective, strategic choice, cuts, beliefs, and gates | `STEERING` | -| Issue-graph projection and soft edges | `COORDINATION` | +| 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/` | @@ -98,6 +98,10 @@ closing control pass. 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/control/COORDINATION.md b/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md deleted file mode 100644 index 365541206a3..00000000000 --- a/libs/@hashintel/brunch-agent/docs/control/COORDINATION.md +++ /dev/null @@ -1,147 +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. The -[steering model](STEERING.md) chooses the current objective, proof frontiers, and cuts under -pressure. This file projects that strategy onto mechanically available work: 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-24**, FE-1476 (the September demo delivery) changes the recommendation from generic -package completion to a concrete CPS review-and-revise proof. FE-1437 crossed the authority -threshold in this branch and is Ready for review; the work remains gated until the branch reaches -`main`. Then open two fronts in parallel. The semantic front starts FE-1482 (the CPS plugin) against -one worked fixture and settles FE-1480's requirements-model-to-SDCPN authority boundary before it -implements a projector; FE-1478 (net-to-requirements provenance) is part of that spine from its -first types. The experience front advances FE-1438 (machine client-tool round-trip) and FE-1439 -(private sessions) far enough for a new reviewer conversation to target an existing document, -while FE-1477/FE-1440 share one provider-routing implementation. Join the fronts at FE-1479 -(targeted re-elicitation), then drive the same path through FE-1423's pre-exposure gates and FE-1441 -deployment. - -FE-1393's generic Gherkin artifact and FE-1387's second-target contract freeze no longer gate the -September proof. FE-1402, FE-1403, FE-1406, and FE-1431 supply only the completion, guidance, -strategy, and contract slices the CPS `review-and-revise` runbook consumes. FE-1481 selects -YAML/Markdown export as the requirements-model inspection floor; broad UI follows only if the -closed loop is already proved. - -```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-1476 [objective] # September reviewer demo - FE-1482 [next, semantic] # concrete CPS plugin + review/revise runbook - FE-1480 [decision, semantic] # model/projection authority, then projector - FE-1478 [semantic proof] # provenance through all three registers - FE-1438 [next, experience] # machine client-tool round-trip + application - FE-1439 [next, experience] # existing-target reviewer session ownership - FE-1477/FE-1440 [experience] # one provider-routing implementation - FE-1479 [join] # targeted correction changes the live net - FE-1481 [fallback] # structured model export before UI - FE-1441 [deployed proof] # HASH deployment - -edges: - FE-1449 -[hard]-> FE-1438 - FE-1438, FE-1439, - FE-1437 -[hard]-> FE-1440 - FE-1437, FE-1439, - FE-1423 -[hard]-> FE-1441 - FE-1437 branch on main -[state-gate]-> FE-1438, FE-1439 - FE-1480 decision -[input]-> FE-1482, FE-1478 - FE-1402, FE-1403, - FE-1406, FE-1431 -[input]-> FE-1482 - FE-1482, FE-1478, - FE-1438, FE-1439 -[input]-> FE-1479 - FE-1479, FE-1440 -[input]-> FE-1441 -``` - -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](../archive/migrations/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 - -- **Projection authority — FE-1480.** The ticket assumes non-deterministic LLM inference from the - requirements model to SDCPN, while ADR-0003 requires write-time-only semantic inference and a - pure projection. A worked CPS transformation must assign every judgment to capture, fold, - projection, or document application before the interface freezes. -- **Controller and runbook.** The harness does not read the folded model or open issues back into - the agent, and no plugin defines a job trajectory or stopping rule. FE-1482 must exercise the - narrow `review-and-revise` loop; FE-1406 and FE-1402/FE-1403 are inputs, not parallel products. -- **Reviewer target identity — FE-1439 × FE-1479.** The current host derives target-document - identity from conversation identity. September requires a new reviewer conversation against an - existing target without weakening owner isolation. -- **Contract freeze — FE-1387.** The CPS target must stress the plugin contract before it freezes. - The freeze follows the September semantic proof rather than gating it. -- **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 currently have no parent. Some are intentional roots; the temporary or -unresolved roots are named here until their Linear parentage is settled: - -- **FE-1331 — start elicitation from Petrinaut's create-new-net flow.** ADR-0004 un-deferred this as - September topology, while FE-1476's new reviewer scenario starts from an existing target. Keep - the conflict visible until Dora confirms the use case and the ADR is amended if necessary. -- **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. -- **FE-1472 — evaluate the nested Anthropic SDK pin.** Unrelated triage root; no owning delivery - map has been chosen. -- **FE-1476 — prepare the September demo.** Temporary delivery root pending the recommended fold - under FE-1357. -- **FE-1477 through FE-1482 — September outcome slices.** PM-authored issues adopted by the - steering model but not yet folded in Linear. The recommended parent is FE-1476; overlaps and - ownership boundaries are recorded in STEERING's issue projection before external mutation. diff --git a/libs/@hashintel/brunch-agent/docs/control/SPEC-LEDGER.md b/libs/@hashintel/brunch-agent/docs/control/SPEC-LEDGER.md index 226a72fac31..fc814343344 100644 --- a/libs/@hashintel/brunch-agent/docs/control/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 index dec69d1e493..41d6f9c3cff 100644 --- a/libs/@hashintel/brunch-agent/docs/control/STEERING.md +++ b/libs/@hashintel/brunch-agent/docs/control/STEERING.md @@ -1,303 +1,93 @@ -# Brunch steering model +# Brunch steering -This is the current strategic understanding of Brunch: what outcome matters now, what the system -can actually do, which beliefs remain weak, and where to spend the next unit of time. Its immediate -decision horizon is the September demo, but it steers the whole context rather than one package or -one Linear map. +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. -This is not a roadmap, a specification, or an issue mirror. Specifications and accepted ADRs define -obligations and settled architecture. Linear is canonical for issue state, hierarchy, and hard -blockers. [COORDINATION](COORDINATION.md) projects this strategy onto the mechanically available -work. This document owns the judgment between them: which objective to pursue under the current -time, evidence, risk, and product pressure; what to defer; and what new evidence would change that -choice. It may expose a conflict with a specification or ADR, but cannot silently override one. +## Objective and acceptance proof -The document remains live. A steering pass orients from current evidence, chooses a frontier, -executes to a named proof, reconciles the result with specifications and issues, then replans only -where the evidence changed. Git carries the history; this file carries only the current model. Its -shape is intentionally specific to the present effort. Do not extract a generic template or skill -until a second real planning cycle shows which parts recur. +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. -## The September outcome +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/). -As of **2026-08-24**, the working constraint is roughly two human weeks. The event date and final -business use case are not yet recorded here; Dora's confirmation is a decision gate, not an excuse -to leave the technical spine vague. +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). -FE-1476 (the September demo delivery) supplies the working scenario: - -1. A reviewer opens a prebuilt cyber-physical-process requirements model and its generated SDCPN in - Petrinaut. -2. The reviewer selects or describes a net element and asks why it was modelled that way. -3. Brunch traces the answer through the requirements model and captures to an exact source - utterance. -4. The reviewer scopes one correction and conducts three to five focused chat turns. -5. New or superseding captures change the elicited model; reprojection changes the corresponding - part of the live net without rebuilding unrelated parts. -6. The resulting artifact is handed to the existing optimisation experiment flow. - -This is a **review-and-revise** demonstration. It does not need to prove that Brunch can elicit an -entire CPS model from a blank conversation. It does need to prove a closed semantic and interaction -loop. A chat transcript beside a static fixture, an unexplained net mutation, or a test that injects -wiring absent from the deployed entrypoint does not satisfy the outcome. - -## External gates - -| Gate | Owner and source | Watch trigger | Last checked | Consequence | -| --- | --- | --- | --- | --- | -| Confirm the business use case and whether review-and-revise supersedes create-new-net for September. | Dora; FE-1476 and the September Plan linked from `docs/INDEX.md` | Dora confirms or changes the scenario. | 2026-08-24 | Replan the proof spine and reconcile ADR-0004 if the confirmed use case requires cold-start creation. | -| Recover or replace the truck-fleet source dossier named by FE-1357. | Owner unresolved/unknown; source: FE-1357 plus historical build ticket FE-1382; FE-1357 names branch `research/truck-fleet-dossier` and `docs/reference/research/truck-fleet-source-dossier.md` | The named branch/ref becomes available, or Frontier 0 selects a different sourced case. | 2026-08-24 — branch/ref unavailable in this checkout | Treat the dossier as an external evidence gap. Do not treat its branch-only path as a missing file in the current tree or use its claims as fixture provenance. | - -The proof spine is therefore: - -```text -source utterance - -> active typed capture - -> folded CPS requirements model - -> SDCPN element + provenance - -> reviewer question and scoped correction - -> superseding capture - -> changed folded model - -> changed live SDCPN - -> optimisation handoff -``` - -## Where the system actually stands - -The package topology is in place and the implemented tracer is real, but the September loop is not -an incremental extension of an almost-finished product. Most of the contract-bearing middle is -absent. - -| Surface | Evidence now | September consequence | -| --- | --- | --- | -| Ask, suspend, return | A user answer to `brunch_ask` survives the AI SDK/Flue boundary and resumes durable history. | Reuse; do not redesign the ask protocol. | -| Settlement and capture | A settled range is privately swept into quote-anchored captures and applied atomically. Supersession and active-head validation exist in the store. | Preserve as the evidence foundation, but expose active state to the controller. | -| Plugin SDK | The exported `Plugin` is deliberately only identity plus exactly one proposal type. Gherkin captures one verbatim statement. | There is no implemented fold, demand runner, model, projection, or useful hard-target plugin to extend. | -| Elicitation control | The agent receives general ask/sweep instructions. Sweep extraction sees a conversation range and proposal names only. No production path reads the active capture set or a derived model back into the interview. | Brunch cannot yet choose a next question from what it has learned or conduct a targeted correction. | -| CPS semantics | The three-register design and provisional two-schema/two-table plugin contract are desk-designed. No `plugin-cps` exists. | The critical semantic path must be built against a concrete CPS case, not inferred from Gherkin completeness. | -| Correction | The store can represent supersession, but extraction cannot see active capture IDs, model issues, or the target region; Gherkin cannot propose a supersession. | Targeted re-elicitation is structurally unreachable despite the storage mechanics being present. | -| Petrinaut transport | Local panel streaming and human ask-return work. Machine client-tool-result follow-ups are explicitly refused pending FE-1438 (the client-tool round-trip). | The agent cannot yet apply a projection to the live document and receive the result. | -| Session target | The current application derives `targetDocumentId` from `conversationId`. | A new reviewer session cannot address a pre-existing elicitation target without changing this identity boundary. | -| Demo website | The production website still uses its stock assistant route. The `/brunch` Actual Mode is a separate read-only fixture/SSE surface. | Local tracer proof must not be mistaken for deployed integration. | - -The decisive reading is that the current design is not too rigorous in its preservation of -evidence, correction, or register boundaries. It is too broad and too generic for the remaining -time. Completing generic plugin machinery, a second target, a full CPS ontology, and a cold-start -interviewer before crossing the real reviewer loop would optimize the library while leaving the -demo hollow. - -## The strategic bet - -Build the smallest honest **CPS review-and-revise loop** through all three registers and the real -Petrinaut entrypoint. Let that concrete implementation discover the minimum plugin interface, then -generalize only what the CPS case and existing Gherkin case both need. - -This is not permission to take another thin tracer as the definition of done. The vertical proof is -contract-bearing: it includes model assembly, provenance, targeted correction, reprojection, -application, and the deployed route. Breadth inside each layer may be narrow; no layer in that loop -may be a fixture masquerading as production wiring. - -The bet preserves these load-bearing decisions: - -- Captures remain the durable, source-grounded assertion register. -- Every semantic inference happens at write time and is recorded as a contestable capture. -- The elicited model is a pure fold over active captures and every model part names its supporting - capture IDs. -- SDCPN projection consumes the elicited model without rereading the transcript or making hidden - semantic judgments. -- Petrinaut application and diagnostics are separate from semantic projection: the application may - use client tools to apply a projected artifact, but it does not become the authority that invents - the model. -- A correction supersedes or adds assertions and re-runs the fold and projection; it does not patch - an unexplained net element directly. - -FE-1480 (requirements-model-to-SDCPN inference) challenges the third and fourth decisions by -assuming the projection itself requires LLM inference. That assumption is unresolved. If a worked -CPS case proves that the register-2 model is insufficient for pure projection, the honest choices -are to record the missing semantic judgment as a capture before folding or to amend ADR-0003 (the -three-register IR) explicitly. Hiding inference inside a read-time projection is not an available -shortcut. - -## The elicitor architecture under this load - -The discussion began with four parts; the current model has five responsibilities across the -harness and plugin layers, plus one per-engagement input. The missing responsibility is the -controller that closes the loop between captured evidence and the next move. - -| Responsibility | Owner | What it contains | State and September obligation | -| --- | --- | --- | --- | -| Strategy repertoire | Harness | Orientations, motivations, conversational licences, interviewing techniques, and question-formulation guidance. | Partly researched, not operationally selected. Implement only the techniques used by the review-and-revise runbook. | -| Evidence engine | Harness | Archive, settlement sweep, quote anchoring, durable captures, issues, conflict, supersession, and provenance primitives. | Strongest implemented layer. Add the active-model/issues read path needed by control and correction; do not broaden storage semantics without evidence. | -| Elicitation controller | Harness | Reads the engagement brief, active folded model and issues, current runbook, and strategy repertoire; chooses `ask`, `propose`, `contrast`, `validate`, `project`, `explain`, or `stop`. | Absent. Build the narrow controller loop needed to explain and revise one selected region. | -| Domain contract | Plugin | Proposal and model schemas; identity, fold, grade, demand, diagnostics, projection, and provenance rules for one target domain. | Designed but unimplemented. Build the CPS subset exercised by the fixture and correction; let it pressure the generic interface. | -| Job runbooks | Plugin | Named jobs over the same domain: objectives, entry conditions, trajectories, demand/completion rules, checks, stopping, revision, boundaries, and handoff. | Absent. Implement `review-and-revise`; defer a complete cold-start runbook. | - -The **engagement brief** is dynamic input, not plugin policy: target document, participant role, -objective, scope, known constraints, allowed actions, and time budget for this run. For September it -binds a reviewer to an existing target and one revisable region. - -A separate free-form “next-question ledger” should not become another authority. Most of it is a -derived control trace: +## Execution tree ```text -runbook demand -> model gap or issue -> candidate move -> chosen move -> concrete ask +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 ``` -Persist only what replay, audit, or explicit user commitment requires. The controller must be able -to explain its chosen move from the runbook and active model; it must not accumulate an independent -shadow plan. +### Active soft edges -The September `review-and-revise` runbook is provisionally: - -```text -entry: - existing target + folded requirements model + projected net + reviewer scope -trajectory: - orient -> select -> explain provenance -> frame correction - -> ask/validate (3-5 turns) -> show semantic and net delta -> confirm -> hand off -done: - scoped demands are met at the declared grade - no open conflict blocks the selected projection - reviewer confirms the intended delta - every changed net element retains provenance -boundary: - do not expand into cold-start elicitation or unrelated net repair -``` +- 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). -## Proof frontiers and execution order +The read-only Linear graph supplies mechanical availability, never priority. -The work has four frontiers. They are ordered by learning dependency, not by which ticket is -currently unblocked. The semantic and experience lanes start in parallel after Frontier 0, then -join as early as possible; they are not two long independent streams to integrate at the end. +## Active gates -### Frontier 0 — make the demo claim decidable - -Confirm the business use case, freeze one representative prebuilt requirements-model/net fixture, -and name the optimisation handoff artifact. On that fixture, settle the FE-1480 authority question: -which steps are write-time semantic capture, pure model fold, pure SDCPN projection, and document -application? - -The prebuilt fixture must be a valid register-1 store state with a source conversation and -quote-anchored captures produced through, or independently validated against, the production -capture/fold path. A hand-authored register-2 model or register-3 net cannot prove provenance and -cannot serve as the correction baseline. - -**Proof:** one reviewed worked transformation in which every SDCPN element needed by the scenario -traces to model fields and captures, with every non-mechanical judgment assigned to a write-time -producer. If this cannot be drawn honestly, implementation should not freeze an interface. - -### Frontier 1 — close the CPS semantic loop - -Implement only the CPS proposal kinds, model slots, identity/fold rules, demands, projection, and -provenance exercised by the fixture and one realistic correction. Carry capture IDs through every -derived layer. Make active model issues and selected-region context available to the controller. - -**Proof:** from the production fold/projection APIs, one source-grounded supersession changes the -expected model field and corresponding SDCPN elements, leaves an unrelated region stable, and -answers both forward and reverse provenance queries. A YAML or Markdown rendering of the model is -enough for inspection at this frontier. The proving proposal must have the shape the production -sweep will emit; the cross-frontier join is not accepted until that sweep produces it from the -reviewer's actual utterance rather than a test inserting it directly. - -### Frontier 2 — close the reviewer control loop - -Allow a new conversation to bind to an existing target document. Admit the machine client-tool -results needed to apply and diagnose a net change. Mount the narrow `review-and-revise` runbook and -controller so that the active model and selected region, rather than the raw transcript alone, -drive three to five questions. - -Session binding and client-tool admission may proceed in parallel with Frontier 1. Mounting the -controller against active model/issues waits for Frontier 1's production read path; do not replace -that dependency with request-shaped model context. - -**Proof:** through the real Brunch HTTP handler and Petrinaut panel, a reviewer selects the prepared -region, receives a grounded explanation, submits a scoped correction, and sees the returned apply -result resume the same durable session. The net delta must trace to a superseding capture produced -by the production sweep from the reviewer's utterance, not one inserted by the test or fabricated -by the controller. No test-only injection supplies the target or tool wiring. - -### Frontier 3 — converge on the deployed demo - -Wire provider/mode routing, browser principal and private session lookup, remote transport, -deployment gates, and the optimisation handoff. Rehearse the exact scenario with a clean browser -against the deployed demo surface. - -**Proof:** a screen-recordable run completes the six September beats, survives one reload, exposes -the before/after requirements-model delta, and hands the resulting SDCPN to the optimisation flow. -Diagnostics show the source capture and projection identities needed to investigate a failure. - -## What is deliberately cut - -Until the proof spine is closed: - -- Do not freeze a broad declarative plugin SDK or require a second hard target. Extract the shared - contract after CPS has stressed it. -- Do not make the Gherkin artifact path a prerequisite for the CPS demo. -- Do not build a full requirements-graph UI. FE-1481's YAML or Markdown export is the selected - fallback; a UI earns time only if the core loop is already green. -- Do not build a complete cold-start CPS interview, general target gallery, every affordance type, - voice input, surprising-scenario generation, or broad telemetry vocabulary. -- Do not implement a comprehensive CPS ontology. Support the fixture, the correction, and the - optimisation handoff while keeping the data model honest about what it omits. -- Do not bypass provenance or write-time semantics to make a visually convincing net mutation. - -These are sequencing cuts, not claims that the deferred obligations are unimportant. - -## Issue projection - -The PM-authored issues are adopted here as the September delivery decomposition. Linear has not yet -been changed; its current unparented state is recorded in COORDINATION until an explicitly approved -registry update. The recommended hierarchy is FE-1357 (September planning and plugin design) → -FE-1476 (September delivery) → FE-1477 through FE-1482. - -| Issue | Strategic role | Reconciliation with existing work | -| --- | --- | --- | -| FE-1476 — prepare the September demo | Outcome owner and acceptance narrative. | Child of FE-1357 while that map remains active; owns rehearsal and handoff rather than implementation details. | -| FE-1477 — route Petrinaut AI and Brunch | Experience-lane entry and mode selection. | Product acceptance overlaps FE-1440 (ship the elicitor in the demo site). Keep one implementation owner; do not build two switches. | -| FE-1478 — trace a generated net to requirements | Provenance acceptance through registers 3 → 2 → 1 → utterance. | Must shape Frontier 1 from its first model/projection types, not arrive as post-hoc metadata. | -| FE-1479 — targeted re-elicitation | Convergence issue for the reviewer loop. | Consumes FE-1438's machine client-tool/application path, FE-1439's session ownership, and the CPS correction path; it does not own a second mutation mechanism. | -| FE-1480 — infer requirements model to SDCPN | Authority and projection decision, then the production projector. | Must be reconciled with ADR-0003 before implementation. FE-1438 owns browser application, not hidden semantic projection. | -| FE-1481 — expose the requirements model | Inspection fallback and demo delta surface. | Select YAML/Markdown first. Defer FE-1442's broader live capture/completion UI unless the proof spine closes early. | -| FE-1482 — add the CPS plugin | Semantic-lane owner and concrete pressure on the plugin boundary. | Pulls the demo-critical slices from FE-1402 (completion), FE-1403 (CPS guidance), FE-1406 (strategies), and FE-1431 (declarative contract). FE-1393 remains the generic/Gherkin path and no longer gates September. | - -Other consequences for the old graph: - -- FE-1387 (second target and plugin-contract freeze) follows the CPS proof instead of preceding the - demo. -- FE-1331 (start from create-new-net) is outside the current reviewer-against-existing-target - scenario, but ADR-0004 explicitly un-deferred it as September topology. The FE-1476 scenario - therefore creates an exposed conflict pending Dora's confirmation and, if review-and-revise - stands, a dated ADR-0004 amendment; this steering document does not silently re-defer it. -- FE-1438, FE-1439, FE-1440, FE-1423 (pre-remote gates), and FE-1441 (deployment) remain real - implementation obligations; the new issues state user outcomes rather than replacing these - substrate and release seams. -- FE-1402, FE-1403, FE-1406, and FE-1431 should produce only what the CPS runbook and domain - contract consume. Their old standalone completion must not become a hidden prerequisite. -- The active Petrinaut integration spec still describes a cold-start interview in some user - stories. Reconcile those stories with the confirmed scenario rather than treating this plan as a - silent specification amendment. +| 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. | -## Beliefs, risks, and replan conditions +## Decision-relevant beliefs and unknowns -| Current belief | Confidence and evidence | Replan when | +| Belief or unknown | Confidence / evidence | Cheapest probe | | --- | --- | --- | -| A bounded review-and-revise scenario can carry the September product claim without cold-start elicitation. | Medium. It is the written FE-1476 scenario, but Dora has not confirmed the use case. | The confirmed use case requires model creation rather than review, or the optimisation handoff requires fields absent from the fixture. | -| A concrete CPS implementation will discover a better minimum plugin contract faster than completing the generic design first. | Medium-high. Gherkin deliberately under-stresses the interface; CPS is the first real consumer. | The first worked CPS transformation cannot be expressed without a reusable harness primitive that must precede it. Build that primitive, then return immediately to the vertical proof. | -| Register 2 can be rich enough for pure SDCPN projection. | Low-medium. ADR-0003 requires it, but no real fold or projector exists and FE-1480 asserts non-determinism. | The worked transformation identifies an unavoidable semantic choice not represented in captures/model. Record it earlier or explicitly revisit the ADR. | -| Three to five turns can produce a meaningful scoped correction. | Low. No CPS runbook has been rehearsed. | Two rehearsals exceed the budget or require unrelated context. Narrow the region, preload explicit context, or revise the demo claim rather than script fake success. | -| The proven ask suspension can extend to document-application client tools. | Medium-low. The suspension spike succeeded, but the production transport intentionally refuses machine tool-result follow-ups. | The first tool round-trip cannot preserve correlation, durability, or non-user evidence semantics. Treat this as a critical integration blocker, not a UI detail. | -| The production website and remote server can be joined inside the timebox. | Medium-low. Local host seams exist; provider routing, session ownership, deployment, and release gates do not yet converge. | A deployable path is not proved by the end of Frontier 2. Preserve the real semantic loop and seek an explicit demo-surface decision rather than quietly falling back to test-only wiring. | -| Requirements-model UI is unnecessary for comprehension. | Medium. The reviewer needs inspectability, but FE-1481 explicitly permits export. | Rehearsal shows that provenance and delta cannot be understood from chat plus a structured export. | - -## Current choice - -Stop treating “unblocked” as “next.” The next strategic move is to run Frontier 0 immediately and -open Frontiers 1 and 2 in parallel: one worked CPS semantic slice and one existing-target reviewer -session/tool-return slice. Join them at the earliest correction, then drive the same bones through -provider routing and deployment. The generic Gherkin/plugin-freeze path and broad UI work wait for -that join. - -Revisit this choice as soon as Dora confirms the use case, the FE-1480 worked transformation lands, -or either parallel frontier fails its first proof. A steering pass that only updates ticket status -does not change this document; a new fact that changes the objective, proof spine, authority -boundary, or cut line does. +| 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..6aac11544cd --- /dev/null +++ b/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md @@ -0,0 +1,53 @@ +# 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/specs/plugin-contract.md b/libs/@hashintel/brunch-agent/docs/specs/plugin-contract.md index a67b4e7bc25..47b587737ca 100644 --- a/libs/@hashintel/brunch-agent/docs/specs/plugin-contract.md +++ b/libs/@hashintel/brunch-agent/docs/specs/plugin-contract.md @@ -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. 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/linear-project-graph.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts index c8ceb357d60..a8cc2f11501 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,93 @@ 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 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 +115,129 @@ 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 } = (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); + expect( + fetchProjectGraph("brunch-agent", false, () => page).issues, + ).toHaveLength(1); + expect( + fetchProjectGraph("brunch-agent", true, () => page).issues, + ).toHaveLength(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", issues: [ { identifier: "FE-100", title: "Build the transport", stateName: "In progress", parentIdentifier: "FE-1", + assignedToViewer: true, external: false, }, { @@ -58,12 +245,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 +261,7 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { title: "Ship the integration", stateName: "Todo", parentIdentifier: "FE-1", + assignedToViewer: true, external: false, }, ], @@ -81,12 +272,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 +287,20 @@ cycles: none`); const renderProjectGraph = await loadRenderProjectGraph(); const graph: ProjectGraph = { projectName: "brunch-agent", + viewerName: "Lu Nelson", 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..a186856a47d 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,7 @@ export interface HardEdge { export interface ProjectGraph { readonly projectName: string; + readonly viewerName: string; readonly issues: readonly ProjectIssue[]; readonly hardEdges: readonly HardEdge[]; } @@ -31,6 +34,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 +57,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 +65,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 +77,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 +203,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}`, `hard=${hardEdges.length}`, + `assignee-mismatches=${assigneeMismatchCount}`, ...(externalCount > 0 ? [`external=${externalCount}`] : []), ].join(" "); const issueLines = [...graph.issues] @@ -218,7 +229,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 +252,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 +270,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 +293,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 +338,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 +397,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 +433,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 +478,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 +497,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 +509,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 +518,7 @@ export const fetchProjectGraph = ( return { projectName: resolvedProjectName, + viewerName: viewer.name, issues: [...issues.values()], hardEdges: [...hardEdges.values()], }; @@ -474,7 +530,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; From 472306fa95652a2fdd22c44db8bee9c29731597a Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 24 Aug 2026 16:41:58 +0200 Subject: [PATCH 10/11] Resolve Brunch control audit discrepancies Carry the graph selection mode into rendering so --all reports an issue total rather than labeling closed issues as open, with a regression test at the CLI projection seam. Exclude Brunch's agent-maintained Markdown from oxfmt and markdownlint, while replacing strategy-log hard breaks with check-clean field paragraphs. Amp-Thread-ID: https://ampcode.com/threads/T-01a0342a-1cca-73fc-9cf1-0f4caac18911 Co-authored-by: Amp --- .markdownlint-cli2.jsonc | 2 +- libs/@hashintel/brunch-agent/AGENTS.md | 1 + .../brunch-agent/docs/control/STRATEGY-LOG.md | 54 ++++++++++++------- .../architecture/linear-project-graph.test.ts | 24 +++++---- .../scripts/linear-project-graph.ts | 4 +- oxfmt.config.ts | 5 +- 6 files changed, 58 insertions(+), 32 deletions(-) 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 fb46bc3be82..9e6a4226a8c 100644 --- a/libs/@hashintel/brunch-agent/AGENTS.md +++ b/libs/@hashintel/brunch-agent/AGENTS.md @@ -26,6 +26,7 @@ workspace. - Use Graphite (`gt`) for stack operations; do not use `gh stack` in HASH. - Issues live in Linear team `FE`, project `brunch-agent`. +- Brunch Markdown is created and maintained by agents; exclude it from `oxfmt` and `markdownlint`. Route by trigger; load only the applicable compact protocol: diff --git a/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md b/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md index 6aac11544cd..606a31e9d20 100644 --- a/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md +++ b/libs/@hashintel/brunch-agent/docs/control/STRATEGY-LOG.md @@ -10,44 +10,62 @@ immutable except for typo/link repair, and only unsuperseded governing IDs appea ### S-001 -**Date:** 2026-08-24 +**Date:** 2026-08-24 + **Trigger/evidence:** FE-1476 defines a bounded reviewer correction scenario; the final use-case -decision remains outstanding. +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. +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. +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 +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 +**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. +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. +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. +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 +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 +**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. +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. +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. +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 +shared prerequisite. + +**Supersedes:** none + **Evidence links:** [STEERING execution tree](STEERING.md#execution-tree), FE-1479 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 a8cc2f11501..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 @@ -28,6 +28,7 @@ interface ProjectIssue { interface ProjectGraph { readonly projectName: string; readonly viewerName: string; + readonly includeClosed: boolean; readonly issues: readonly ProjectIssue[]; readonly hardEdges: readonly { readonly from: string; readonly to: string }[]; } @@ -203,9 +204,8 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { }); test("defaults to open issues and --all includes closed issues", async () => { - const { fetchProjectGraph, parseArguments } = (await import( - SCRIPT_MODULE_URL - )) as LinearProjectGraphModule; + const { fetchProjectGraph, parseArguments, renderProjectGraph } = + (await import(SCRIPT_MODULE_URL)) as LinearProjectGraphModule; const page: ProjectIssuePage = { projectName: "brunch-agent", viewer: { id: "viewer-id", name: "Lu" }, @@ -218,12 +218,16 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { }; expect(parseArguments([]).includeClosed).toBe(false); expect(parseArguments(["--all"]).includeClosed).toBe(true); - expect( - fetchProjectGraph("brunch-agent", false, () => page).issues, - ).toHaveLength(1); - expect( - fetchProjectGraph("brunch-agent", true, () => page).issues, - ).toHaveLength(2); + 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 () => { @@ -231,6 +235,7 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { const graph: ProjectGraph = { projectName: "brunch-agent", viewerName: "Lu Nelson", + includeClosed: false, issues: [ { identifier: "FE-100", @@ -288,6 +293,7 @@ cycles: none`); const graph: ProjectGraph = { projectName: "brunch-agent", viewerName: "Lu Nelson", + includeClosed: false, issues: [ { identifier: "FE-100", diff --git a/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts b/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts index a186856a47d..7d8ee640ff7 100644 --- a/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts +++ b/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts @@ -20,6 +20,7 @@ export interface HardEdge { export interface ProjectGraph { readonly projectName: string; readonly viewerName: string; + readonly includeClosed: boolean; readonly issues: readonly ProjectIssue[]; readonly hardEdges: readonly HardEdge[]; } @@ -208,7 +209,7 @@ export function renderProjectGraph(graph: ProjectGraph): string { ).length; const header = [ `project ${graph.projectName}`, - `open=${projectIssueCount}`, + `${graph.includeClosed ? "issues" : "open"}=${projectIssueCount}`, `hard=${hardEdges.length}`, `assignee-mismatches=${assigneeMismatchCount}`, ...(externalCount > 0 ? [`external=${externalCount}`] : []), @@ -519,6 +520,7 @@ export const fetchProjectGraph = ( return { projectName: resolvedProjectName, viewerName: viewer.name, + includeClosed, issues: [...issues.values()], hardEdges: [...hardEdges.values()], }; 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 From 35b330786e2e99dc45f1d39fe97420f0db4e4cdc Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 25 Aug 2026 14:51:12 +0200 Subject: [PATCH 11/11] Format imported SDCPN layouts Normalize final newlines so the repository-wide formatting gate accepts the imported layout fixtures. Co-authored-by: Cursor --- .../inbox/SDCPNs/gases-1-pn-consumption-trigger-layout.json | 2 +- .../brunch-agent/docs/inbox/SDCPNs/gases-1-pn-layout.json | 2 +- .../brunch-agent/docs/inbox/SDCPNs/gases-2-spn-layout.json | 2 +- .../brunch-agent/docs/inbox/SDCPNs/gases-3-cpn-layout.json | 2 +- .../brunch-agent/docs/inbox/SDCPNs/gases-4-dcpn-layout.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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 index 16be4b70b64..fd8d6e3fadd 100644 --- 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 @@ -483,4 +483,4 @@ "generator": "Petrinaut" }, "title": "Gases 1 — plain net, one customer (consumption trigger)" -} \ No newline at end of file +} 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 index c5fa9b7e740..d4b42b96572 100644 --- 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 @@ -469,4 +469,4 @@ "generator": "Petrinaut" }, "title": "Gases 1 — plain net, one customer" -} \ No newline at end of file +} 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 index 9e69eac225e..31eb77a1c1a 100644 --- 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 @@ -975,4 +975,4 @@ "generator": "Petrinaut" }, "title": "Gases 2 — stochastic net, two customers on one tanker" -} \ No newline at end of file +} 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 index 1bb69ebe485..6c0226d031e 100644 --- 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 @@ -1297,4 +1297,4 @@ "generator": "Petrinaut" }, "title": "Gases 3 \u2014 coloured net, three customers and a mixed fleet" -} \ No newline at end of file +} 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 index 3f8a50e3d5d..cf0b2b1947f 100644 --- 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 @@ -1616,4 +1616,4 @@ "generator": "Petrinaut" }, "title": "Gases 4 \u2014 dynamic coloured net" -} \ No newline at end of file +}