Skip to content

release: promote 2026-Q3 to master - #45

Merged
michaldengusiak merged 100 commits into
HoareLea:masterfrom
SAM-BIM:sow/2026-Q3
Oct 6, 2026
Merged

michaldengusiak merged 100 commits into
HoareLea:masterfrom
SAM-BIM:sow/2026-Q3

Conversation

@michaldengusiak

Copy link
Copy Markdown
Contributor

2026-Q3 quarter-close promotion (Hoare Lea promotion pilot)

This PR promotes SAM-BIM's sow/2026-Q3 branch to Hoare Lea master, per the standard SAM quarter-close release convention.

Source (SAM-BIM sow/2026-Q3): 05ca0c187bd8647b4ea105dd8c0e230000ccfeab
Target (Hoare Lea master): a8cb990db1e9235debcd27ad09d1269c1d4c1802

Hoare Lea master is a clean ancestor of sow/2026-Q3 (60 commits ahead, no divergence, no unrelated-history issue).

Q3 scope summary (from git history)

  • Part O: PR5a/PR5b unit settings and recirculation cooling, DHW circuit-group design temperature difference, iteration 3 template plant-carriage tests and systems materialisation, ventilation-unit catalogue (including XBC15) and its late-P2 hardening, Nuaire ventilation-unit capacity fix.
  • Part F: terminal transfer compliance.

CI / checks

Combined check-runs on Q3 HEAD (05ca0c18): PASS — build success.

Notes

  • This is the 2026-Q3 quarter-close promotion. No 2026-Q4 work is included or intended in this PR.
  • Merge convention: merge commit, no squash, no rebase — to be merged in a later, separate step after review.
  • This PR is opened as part of a four-repository pilot batch (SAM, SAM_UI, SAM_Systems, SAM_Tas). No cross-repo merge-ordering dependency is required, but reviewers should be aware the other three pilot PRs land from the same Q3 cut.
  • Synchronising SAM-BIM master with Hoare Lea master is a later, separate step and is explicitly out of scope for this PR.

🤖 Generated with Claude Code

michaldengusiak and others added 30 commits June 28, 2026 19:40
…llier (#7)

* feat: add SAM.Analytical.Systems.Mollier bridge (Mollier -> SystemEnergyCentre)

Introduce a new netstandard2.0 assembly that converts a chain of psychrometric
Mollier processes into a connected, simulation-ready SAM_Systems air-handling
system, closing the loop from psychrometric design to detailed simulation.

- Create.SystemComponent: per-process -> ISystemComponent mapping (cooling/heating
  coil, fan, exchanger, humidifier, air junction), deriving Setpoint, Duty and
  cooling BypassFactor from process end-states and a design airflow.
- Create.SystemPlantRoom / Create.SystemEnergyCentre: order the process chain and
  wire components sequentially onto one AirSystem via SystemPlantRoom.Connect.
- Query.Duty / Query.MassFlow / Query.BypassFactor / Query.SystemComponentType:
  reusable psychrometric -> systems helpers.
- Twin-wheel (latent) recovery flagged when a HeatRecoveryProcess shifts humidity.
- Cross-repo coupling without a circular reference: SAM.*.Mollier consumed as
  built DLLs from the sibling SAM_Mollier repo (build SAM + SAM_Mollier first).
- Register project in SAM_Systems.sln; add docs/Mollier-Systems-Bridge.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* feat: add Grasshopper node and AirHandlingUnitResult overload for Mollier bridge

- SAMSystemsCreateEnergyCentreByMollier (SAM.Analytical.Grasshopper.Systems):
  GH component taking a list of Mollier processes, a design airflow and a name,
  outputting a SystemEnergyCentre via the bridge. Reuses GooMollierProcessParam.
- Grasshopper.Systems csproj: reference the bridge project plus SAM.Core.Mollier
  and SAM.Geometry.Grasshopper.Mollier from the sibling SAM_Mollier build.
- Create.SystemEnergyCentre overloads that source the design airflow from
  AirHandlingUnitResult.SupplyAirFlow (the "pull from model" path).
- Qualify internal Create.* calls and refresh docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* feat: twin-wheel supply+extract support for Mollier bridge

Add Create.SystemPlantRoom/SystemEnergyCentre overloads that take a supply and an
extract Mollier process chain and share a single SystemExchanger across both air
paths. Connect auto-selects the first unconnected connector pair, so the supply
chain takes air path 1 and the extract chain takes air path 2 of the same device,
modelling latent + sensible recovery (twin-wheel) as one exchanger. Heat-recovery
devices are reused across chains paired in order.

The Grasshopper node gains optional _extractMollierProcesses_ / _extractAirflow_
inputs and routes to the twin-wheel overload when an extract chain is supplied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* docs: add twin-wheel worked example for the Mollier bridge

Add an end-to-end example to docs/Mollier-Systems-Bridge.md showing how to build a
twin-wheel AHU as supply/extract Mollier process chains (verified factory signatures)
and auto-generate the connected SystemEnergyCentre via the bridge, including the
JSON round-trip hand-off point for Tas. Supports the CESBP-2025 validation case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* feat: add two Mollier GH nodes and comprehensive component/input descriptions

- SAMSystems.CreatePlantRoomByMollier: Mollier processes -> SystemPlantRoom
  (supply + optional twin-wheel extract), for merging into an existing energy centre.
- SAMSystems.CreateComponentByMollierProcess: one process -> SystemComponent plus
  derived duty [W] and cooling bypass factor, the per-process building block.
- Rewrite all Grasshopper component, input and output descriptions to be
  comprehensive: units, intent, process->component mapping table and twin-wheel
  behaviour now appear in every tooltip.
- Update docs with a component reference table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* test: add twin heat-recovery worked example with self-check

- TwinWheelExample (bridge assembly): builds the supply/extract Mollier process
  chains for a twin-wheel AHU, converts them to a SystemEnergyCentre via the factory,
  and provides a framework-free Verify() running deterministic checks (component
  creation, single plant room, JSON round-trip, cooling duty and bypass factor).
- SAMSystems.MollierTwinWheelExample GH node: runs the example and outputs the energy
  centre, a PASS/FAIL report and an overall success flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* docs(gh): comprehensive descriptions for system query/add components (1/4)

Rewrite component summaries and all input/output tooltips for AddSystemEnergyCentre,
ConnectedSensor, ConnectedSystemComponents, ComponentType, ConnectedSystemGroups and
ConnectedSystemObjects. Also fix two copy-pasted summaries (ConnectedSystemGroups and
ConnectedSystemObjects previously read "Gets Connected System Components").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* docs(gh): comprehensive descriptions for connector/display components (2/4)

Rewrite summaries and all input/output tooltips for SystemConnector,
CreateDisplaySystemConnectorManager, CreateDisplaySystemManager and
CreateSystemGeometrySymbol.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* docs(gh): comprehensive descriptions for query/display components (3/4)

Rewrite summaries and I/O tooltips for OrderedSystemComponents,
PlantRoomRelatedObjects and SystemToAHU.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* docs(gh): comprehensive descriptions for remaining components (4/4)

Rewrite summaries and I/O tooltips for ModifyAirSystem, UpdateVentilationSystem,
MergeEnergyCentreByECs, ResultValueByHourOfYear, AnalyticalSystemType, and polish
CreateSystemEnergyCentre. Fill the empty summary on
CreateSystemEnergyCentreByMechanicalSystems and fix the broken MergeEnergyCentreByECs
summary. Completes the repo-wide Grasshopper description pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* refactor: single-chain plant-room builder reuses shared AddChain helper

The single-chain Create.SystemPlantRoom now delegates to the AddChain helper used by
the supply/extract (twin-wheel) builder instead of duplicating the map-and-wire loop.
Behaviour is unchanged; the chain-walking logic now lives in one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* feat: derive heat-recovery effectiveness for twin-wheel exchangers

Compute each shared SystemExchanger's sensible and latent effectiveness from both air
paths (supply-side definition, fractions 0-1) and write them to the exchanger, with
ExchangerCalculationMethod/ExchangerType set to Simple so the values are used.

- Query.HeatRecoveryEfficiencies(supplyProcess, extractProcess, out sensible, out latent).
- Create.SystemPlantRoom(supply, extract, ...) pairs supply/extract heat-recovery
  processes in order and applies the efficiencies to the shared exchangers.
- Single-chain mapping still flags latent capability but leaves efficiencies unset
  (one process cannot determine effectiveness).
- Extend TwinWheelExample.Verify with effectiveness checks; update docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* feat: set cooling-coil MinimumOffcoil to the Apparatus Dew Point

Air cannot leave a cooling coil colder than the coil surface (~the ADP), so set
SystemCoolingCoil.MinimumOffcoil to the ADP dry-bulb temperature derived from the
cooling process. Update the mapping table in the docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* docs: add HANDOVER.md for session continuity

Capture the Abstract 2 Mollier->HVAC bridge state: scope, branches/PRs, implemented
files, verified API facts, deliberate omissions and next steps, so work can resume in
a new session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBPdu7fogKggmF47N76sXM

* ci: build SAM_Mollier dependency + add SPDX headers; qualify bridge Create

- build.yml: add SAM_Mollier to the sibling clone/build order so the
  SAM.Analytical.Systems.Mollier project can resolve the SAM.*.Mollier
  HintPath references (fixes CS0234/CS0246 in CI).
- Add the required SPDX/copyright header to 10 changed Grasshopper
  component files flagged by the SPDX check.
- TwinWheelExample: fully-qualify Create.SystemEnergyCentre so the name
  binds to the bridge Create type rather than the enclosing Create method.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwH8BymskFGrsoKZz7k5ab

* fix: use compiled HeatRecoveryProcess_Supply/_Extract in TwinWheelExample

The example called MollierPoint.HeatRecoveryProcess(...), but that overload
lives in Create\HeatRecoveryProcess.cs which SAM.Core.Mollier.csproj excludes
via <Compile Remove>. The compiled API is HeatRecoveryProcess_Supply /
HeatRecoveryProcess_Extract. Switch the supply/extract chains to those, and
pass the efficiencies as percentages (75/65) since the methods divide by 100
(0.75 previously meant 0.75% recovery). Fixes CS1061 in the CI build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwH8BymskFGrsoKZz7k5ab

* build: reference SAM.Analytical.Mollier from Grasshopper.Systems project

SAMSystemsCreateEnergyCentreByMollier uses AirHandlingUnitResult, whose
type lives in SAM.Analytical.Mollier. The Grasshopper project referenced
SAM.Core.Mollier and SAM.Geometry.Grasshopper.Mollier but not
SAM.Analytical.Mollier, causing CS0012. Add the missing assembly
reference (HintPath into the sibling SAM_Mollier build output).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwH8BymskFGrsoKZz7k5ab

* fix: persist resolved connector indexes in CreateSystemConnection

SystemPlantRoom.CreateSystemConnection resolved the In/Out connector indexes
via TryGetIndexes (index_1_out/index_2_out) but then constructed the
SystemConnection with the raw index_1/index_2 arguments. When a caller passes
-1 (auto-select), the connection was stored with -1, so the connectors never
read as occupied and subsequent links reused the same connector, mis-wiring
the generated topology. Build the connection with the resolved indexes
instead; explicit-index callers are unaffected (TryGetIndexes returns those
unchanged). Addresses the Codex P1 on the Mollier bridge twin-wheel wiring.

Also refresh docs/HANDOVER.md with current PR/CI status and the fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hXuHWUCWPeNTmo27SzoEk

* fix: persist twin-wheel HR efficiencies and relate single-component chains

Addresses the two open Codex findings on the Mollier->HVAC bridge.

P1 (SupplyExtract.cs): SystemPlantRoom.Add clones the component, so the
exchanger list held the pre-Add original while the plant room stored a clone;
ApplyHeatRecoveryEfficiencies mutated the detached original, so the stored and
serialised exchanger had no sensible/latent effectiveness and twin-wheel
recovery was silently dropped. It now looks the exchanger up by Guid in the
plant room, sets the efficiencies on that stored instance and re-adds it
(re-adding under the same Guid replaces in place, relations intact). Wiring was
already correct (CreateSystemConnection resolves connectors from the live
cluster by Guid).

P2 (SupplyExtract.cs): a single-process chain never reached the pairwise
Connect, so its lone component was never related to its AirSystem and
GetSystemComponents<T>(ISystem) saw an empty plant room. AddChain now relates
the first component to its air system when there is no previous component.

TwinWheelExample.Verify strengthened to guard both: it reads the
sensible/latent effectiveness back from the JSON-round-tripped stored exchanger
and asserts a single-component plant room relates its component to its air
system. Verified locally - all 14 checks pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: wire Mollier chains on Out->In connectors (Codex P1)

After the connector-index root fix, SystemPlantRoom.Connect with index -1
auto-selects each component's first unconnected connector. Air components
define their In connector before Out, so an auto-wired step (e.g. heat
recovery -> cooling) was stored as previous.In -> current.Out, leaving the
upstream Out connector free; GetOrderedSystemComponents(..., Direction.Out)
and the export/conversion paths could not follow the generated chain.

AddChain now resolves explicit connectors: the first UNCONNECTED Out on the
upstream component and the first UNCONNECTED In on the downstream one, then
passes them to Connect. Picking the first unconnected (not just the first)
connector also routes the shared twin-wheel exchanger's second air path onto
its second connector pair instead of colliding with the first. Falls back to
auto-selection if a directional connector cannot be resolved.

TwinWheelExample.Verify gains an Out-direction reachability check (the supply
head's Out-walk must reach its 3 downstream components; the In<->Out bug caps
it at 0). Verified locally - all 15 checks pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
…able

Bridge merged to sow/2026-Q3 (Codex findings fixed + verified, build order
fixed across SAM_Deploy and the 5 downstream CIs). Update HANDOVER so all tasks
read done except the post-merge sow build confirmation, and record the real
Abstract 2 deliverable (the SAM_Tas TPD export + CESBP-2025 validation) as the
next step on a new branch.

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TwinWheelExample produces a logical SystemEnergyCentre with no display
geometry, so it does not preview in the Grasshopper viewport (unlike the
laid-out MVRE.json). Document the root cause and a plan to add an
auto-layout converter (logical -> DisplaySystemEnergyCentre) backed by the
existing SAM_DisplaySystemManager.JSON symbol library.

Coverage note: 5 of the 6 bridge-emitted component types have symbols;
base SystemHumidifier is the only gap (missing from both the symbol
library and Create.DisplayObject<T>).

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…symbols

Per design direction: SystemHumidifier becomes the abstract base for its three
concrete subtypes (SystemDirectEvaporativeCooler, SystemSprayHumidifier,
SystemSteamHumidifier), each of which already has a symbol in
SAM_DisplaySystemManager.JSON. The bridge maps the Mollier HumidificationProcess
subtype to a concrete humidifier (Adiabatic -> Spray, Isothermal/Steam ->
Steam), closing the only coverage gap (now 6/6, no resource change needed).

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…temHumidifier

Implements the schematic-preview plan so a logical SystemEnergyCentre from the
Mollier -> SAM_Systems bridge can be drawn/baked in Grasshopper.

- Create.DisplaySystemEnergyCentre/DisplaySystemPlantRoom: auto-layout converter
  (one row per air system, flow order) that converts each component to its display
  symbol and re-creates each connection; components without a symbol are reported.
- DisplaySystemPlantRoom.CreateSystemConnection: routes an orthogonal polyline
  between the two components' connector world points (geometry-aware index fallback;
  centroid fallback) instead of throwing NotImplementedException.
- SystemHumidifier made abstract; bridge maps adiabatic humidification -> spray and
  isothermal/steam -> steam humidifier (Create.SystemComponent, Query.SystemComponentType),
  so every emitted humidifier has a concrete, drawable symbol.
- Grasshopper: new SAMSystems.CreateDisplaySystemEnergyCentre node; _display_ toggle
  plus displaySystemObjects/displaySystemPlantRooms outputs on the twin-wheel example.
- TwinWheelExample: CreateDisplay helper + Verify display checks (drawable geometry,
  connection count preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsKxVe4Rozn5AL4kPH8fGu
- DisplaySystemPlantRoom / DisplaySystemEnergyCentre: drop `using SAM.Core;`
  (caused CS0104 'ISystem' ambiguity vs SAM.Core.Systems.ISystem); fully-qualify
  SAM.Core.Direction and SAM.Core.SAMObject instead. Fixes the build job.
- CreateSystemConnection: when the air system is known but no unconnected Out/In
  connector pair can be resolved, return null instead of fabricating a connection
  on an invalid/occupied connector.
- DisplaySystemEnergyCentre converter: track display membership per (air system,
  component) pair instead of a global connected set, so a component shared across
  air systems is related to every system it belongs to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsKxVe4Rozn5AL4kPH8fGu
…entre

Per review feedback, the Mollier bridge now builds drawable output by default so
SAMSystems.MollierTwinWheelExample yields a SystemEnergyCentre that previews in
Grasshopper without any extra conversion step.

- Create.SystemPlantRoom (single-chain and supply/extract) promotes its logical
  plant room to a DisplaySystemPlantRoom: every component becomes its DisplaySystem*
  equivalent (symbol + auto layout) and every connection a routed polyline. This is
  done at plant-room assembly, where the symbol manager and layout are available
  (the per-process Create.SystemComponent factory has neither). Falls back to the
  logical room when no symbol library is present. DisplaySystem* are subclasses of
  their System* types, so simulation/export is unaffected.
- TwinWheelExample.Verify: checks the bridge output is display-native (drawable
  components + connections) directly, instead of re-converting; skips gracefully
  when no symbol library is available. Removed the now-redundant CreateDisplay helper.
- SAMSystems.MollierTwinWheelExample node: the systemEnergyCentre output is now
  drawable; it is flattened to displaySystemObjects for direct preview/bake. Dropped
  the obsolete _display_ toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsKxVe4Rozn5AL4kPH8fGu
SystemPlantRoom.Connect dereferences the relation cluster (Contains) before it is
lazily created, and the cluster is only created by Add. The display converter's
first mutation was a Connect call, so a multi-component conversion threw a
NullReferenceException. Seed the DisplaySystemPlantRoom with its air systems and
converted components (via Add, which creates the cluster) before any Connect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsKxVe4Rozn5AL4kPH8fGu
fix object generation if null
Addresses the PR #8 review request to build the Mollier example on a real
plant-room template (Plantroom-Only.json) instead of emitting an air-only
SystemEnergyCentre, so the result keeps the template's liquid systems, energy
sources and plant equipment and is simulation-ready.

- Query.SystemEnergyCentre(path) / SystemEnergyCentre_FromResources(fileName):
  load a template energy centre from a SAM JSON file; bundle Plantroom-Only.json
  under files/resources/Analytical/Systems/SystemEnergyCentre/.
- Create.SystemEnergyCentre(..., SystemEnergyCentre template): build the air side
  from the Mollier processes and copy each air system (with its components and
  routed display connections) into a copy of the template's first plant room via
  the existing SystemPlantRoom.CopyFrom transitive copy. The template object is
  never mutated; a null template falls back to the previous air-only behaviour.
- TwinWheelExample.Create(..., template) overload.
- SAMSystems.MollierTwinWheelExample and SAMSystems.CreateEnergyCentreByMollier
  gain an optional _plantRoomTemplate_ file-path input; the example defaults to
  the bundled Plantroom-Only.json when no path is given.

Deferred (need a Grasshopper viewport to verify and a maintainer design call):
layout offset to avoid air/plant overlap, Junction Fresh/Exhaust Air insertion,
DisplayAirSystemGroup per case, second heat-recovery offset, and the
CreateAirSystemByMollier rename with an AirSystem-typed output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
When seeding from a plant-room template, the bridge laid the Mollier air side
out from the origin - the same region where the template's plant equipment
sits - so the two schematics overlapped in the viewport (the maintainer's
"layout is not correct" note).

Before copying the air systems into the template plant room, translate the
air-side display objects (components and connections) so their combined extent
sits directly above the template's display extent with a clear margin, left
edges aligned. Connections move with their components (ISystemConnection is an
ISystemComponent) and each moved object is re-added under its existing Guid, so
all relations are preserved. Geometry-only: the logical model, JSON round-trip
and simulation are unaffected, and the step no-ops when either side carries no
display geometry (logical fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
…unctions

Addresses the PR #8 request that air paths not be left dangling at outside-air
conditions. The bridge now adds explicit air junctions at the two outside-air
boundaries of the generated air-handling plant room:

- "Junction Fresh Air" feeds the supply intake (the supply system's open In),
  wired junction.Out -> component.In.
- "Junction Exhaust Air" terminates the extract discharge (the extract system's
  open Out), wired component.Out -> junction.In - added only when an extract
  chain is present.

The room-side connectors (supply -> room, room -> extract) are deliberately left
open: those are room-side, not outside-air, boundaries. Connector indexes follow
the SystemJunction layout (In=0, Out=1), matching the reference HeatRecovery-
Junctions plant room. Junctions are skipped when the boundary component is already
a junction. SystemAirJunction has a symbol in the bundled DisplaySystemManager and
Create.DisplayObject maps it to DisplaySystemAirJunction, so the junctions draw in
the preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
Extend the framework-free self-check so the bridge's fresh/exhaust junction
capping is covered: the report now verifies that a "Junction Fresh Air" and a
"Junction Exhaust Air" exist on the generated plant room and that each is wired
into its chain. These are logical-model checks, so they hold even in a headless
context where the symbol library is unavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
Per maintainer correction on PR #8: all objects on the supply and extract streams
form a single AirSystem, not two separate ones. AddChain now wires both chains
onto one shared AirSystem ("Air System"). The shared twin-wheel exchanger still
consumes air path 1 on the supply chain and air path 2 on the extract chain, via
the existing first-unconnected-connector selection.

Because the two streams now share an air system, the outside-air junctions can no
longer be found by an ambiguous open-connector query (there would be two open Ins
and two open Outs). AddChain therefore reports each chain's first and last
components, and the fresh/exhaust junctions are placed on the specific boundary
components: fresh air on the supply chain's first component (open In), exhaust air
on the extract chain's last component (open Out). Room-side boundaries (supply
discharge, extract intake) remain open.

Note: with one air system the default converter lays all components on one row;
the two-row (supply / mirrored extract) coordinate layout and the room-side
DisplayAirSystemGroup are the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
…ystemGroup

Implements the room/group arrangement (PR #8 item 5), grounded in the reference
airsystem-withPlantoom.json.

The bridge now closes the air loop through the room: supply discharge -> Group
Junction -> Damper -> Room (Space) -> Group Junction -> extract intake, all on the
single air system. The room Space is created from the Mollier room condition point
(dry-bulb temperature and relative humidity become setpoints; area/volume/flow
sizing are placeholders, as the Mollier processes carry no space geometry). Once
laid out, the room-side items (Room, Damper, both Group Junctions) are wrapped in a
DisplayAirSystemGroup named after the air system and related to it, matching the
reference object graph.

Verify covers the new structure: room and damper present, single shared air
system, and (when the symbol library is available) a DisplayAirSystemGroup.

Note: layout is still the converter's single-row flow order; the two-row
supply/mirrored-extract coordinate layout the maintainer described is the
remaining visual step and needs a Grasshopper viewport to tune.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D272omUvR5c6173iECqTvy
… on SAMAnalyticalSystemResults (#11)

Fixes CS0535 build errors when downstream repos (e.g. SAM_Revit) pull in
sow/2026-Q3, since IGH_SAMComponent gained these members and this was the
only implementer in this repo missing them.
- SAMAnalyticalSystemUpdateAirSystem: fix inverted null check (airSystem != null -> == null)
- SAMAnalyticalSystemCreateDisplaySystemConnectorManager: fix wrong loop variable (displaySystemConnectors -> systemObjects)
… tested topology and layout (#9)

* feat(mollier-bridge): structured diagnostics, humidifier/fan derivation, two-row layout, liquid systems, tests

* docs: record P1-P10 delivery status in implementation plan

* chore: add SPDX license headers to test files and build scripts

* fix: add MinCompatibleVersion and ObsoleteSeverity to SAMAnalyticalSystemResults for IGH_SAMComponent compat

* chore: add SPDX license header to SAMAnalyticalSystemResults.cs

* revert: remove premature ObsoleteSeverity/MinCompatibleVersion from SAMAnalyticalSystemResults (SAM PR #36 not yet merged)

* fix: re-add MinCompatibleVersion and ObsoleteSeverity to SAMAnalyticalSystemResults

* test: make the Mollier bridge suite prove behaviour instead of executing it

- Test project now outputs to repo-local build_tests\ (was ..\..\build_tests\,
  escaping the repository and never matching test.ps1's search path) and copies
  MVRE.json + SAM_DisplaySystemManager.JSON to the output, so the MVRE fixture
  and the default symbol library resolve deterministically on any machine.
- MVRE_ComparisonTests: a missing fixture now fails loudly instead of silently
  returning (all 5 tests previously passed without ever loading MVRE.json).
  Replaced output-only comparisons and Assert.True(true) with exact structural
  assertions against the verified fixture census (1 plant room, 2 energy
  sources by name, 2 fans, 1 exchanger, 1 damper, 1 space, 25 connections) and
  both-side air-topology checks; the intentionally-uncompared liquid-plant
  asymmetry is documented in the tests.
- HeatRecoveryProcess_MapsTo_SystemExchanger now builds a real process via the
  public HeatRecoveryProcess_Supply factory (the old "internal constructor"
  comment was wrong) and asserts setpoint, calculation method and latent type.
- Exchanger_RoundTrip_PreservesEfficiency now sets and round-trips real
  sensible/latent efficiencies instead of asserting NotNull on a bare object.
- test.ps1: missing test DLL, failed Verify run and "no tests matched" all
  exit 1 and print the captured output; build.ps1 prints build output on
  failure. Previously the Verify re-run warned and exited 0 unconditionally.
- .gitignore: ignore the now repo-local build_tests/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: canonical diagnostics contract with emissions at every failure path

One code, one meaning, one severity - documented on every DiagnosticCodes
constant and enforced end to end:

- Renamed four never-emitted constants to the meanings the bridge actually
  needs: 003 InvalidProcessState (was InvalidEndPoint), 009 ConnectionFailed
  (was MoistureTransferDetected), 010 DisplaySymbolMissing (was
  HeatRecoveryMissingExtract), 013 HeatRecoveryCountMismatch (was
  ConversionWithWarnings). All 14 codes now have XML docs.
- Fixed MOLLIER-012 misuse: null chain/group arguments now emit 005
  NullProcessChain (Error); 012 ChainEmpty is reserved for chains that
  produced zero components.
- New emission sites: 002 null process, 003 invalid start/end state,
  006 ADP unavailable, 007 bypass factor NaN, 008 heat-recovery pair with no
  usable effectiveness, 009 every failed Connect and every directional-to-
  automatic connector fallback (AddChain, boundary junctions, room loop,
  display group), 010 display promotion report lines incl. missing symbol
  library, 013 supply/extract heat-recovery count mismatch.
- Liquid loop wiring failures (index unresolved or Connect false) now emit
  014 LiquidInjectionFailed as Error - a partially wired loop is no longer a
  silent success; severity raised from Warning on the existing sites too.
- Template and AirHandlingUnitResult entry points gained out-diagnostics
  overloads; the old signatures remain as wrappers. InjectLiquidSystems
  diagnostics are no longer discarded internally.
- Deleted dead ConversionResult (never constructed or referenced).
- ConversionDiagnosticTests: 8 new/strengthened tests prove 002, 003, 005,
  006+007, 012 severity, 013, template-overload propagation and code
  uniqueness. 83 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: correct fan pressure physics, humidifier %RH setpoints, steam duty and default energy sources

Fan pressure (Query/FanPressure.cs): the efficiency now MULTIPLIES.
The bridge recovers pressure from the fan temperature pickup, and SAM's own
convention (SAM.Core.Mollier Query.PickupTemperature) is dT = SFP/(rho*cp)
with SFP = dP/eta, hence dP = eta*rho*cp*dT. The previous dP = rho*cp*dT/eta
overstated pressure by 1/eta^2 (~2x at eta 0.7). cp now comes from
SpecificHeatCapacity_Air at the inlet state instead of a hardcoded 1010, so
FanPressureRise is the exact inverse of PickupTemperature: for a process
built with FanProcess(start, sfp), dP == 1000*eta*sfp identically.

Humidifier setpoint (Query/HumidifierProperties.cs): now the end-state
RELATIVE HUMIDITY [%] for both spray and steam. SAM humidifier setpoints
pass straight through to Tas TPD (Convert.ToTPD SprayHumidifier/
SteamHumidifier), whose humidifiers control downstream %RH under default
flags (TPD template code sets Setpoint.Value = 90). The previous end
dry-bulb degC value was the wrong controlled variable.

Steam duty: removed the |dT| < 0.01 isothermality gate. Library-built
SteamHumidificationProcess is deliberately not isothermal (injected steam
carries sensible heat, dT ~ +0.3 K), so the gate never fired and Duty was
never set on any SystemSteamHumidifier.

Fan design flow (Create/SystemComponent.cs): stored in litres per second
(TPD fan flow unit; template code uses values like 150) with
DesignFlowType = Value, instead of the raw m3/s figure with type None.
Existing test expectation updated accordingly.

Default energy sources (Create/LiquidSystem.cs + entry points): template-
less energy centres now carry "Natural Gas" (CO2 0.216, cost 0.05) and
"Grid Supplied Electricity" (ElectricalEnergySource, CO2 0.519, cost 0.13)
SystemEnergySources mirroring the MVRE.json reference, with the boiler and
chiller stamped with SystemObjectParameter.EnergySourceName (+
FanEnergySourceName for the chiller) before Add - exactly the name-based
linkage SAM_Tas resolves into TPD FuelSources. Template paths are
unchanged: the template keeps supplying its own sources and plant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: give the twin-wheel example fans a real temperature rise

The example's fans produced no pressure at all. SAM.Core.Mollier's
Create.FanProcess(MollierPoint, double) assigns the pickup temperature - a
RISE in K, sfp/(rho*cp) - as the End point's ABSOLUTE dry-bulb temperature,
so the supply fan entering at 16 C "left" at 0.65 C and the extract fan at
30 C left at 0.68 C. Both rises were therefore negative, FanPressureRise
correctly returned NaN for them, and the resulting SystemFans carried
Pressure = 0 with OverallEfficiency unset - the exact opposite of what the
example is meant to demonstrate. (The factory's four-argument sibling adds
the pickup to the inlet temperature, confirming the inconsistency.)

Build the fan End state here from the sound Query.PickupTemperature
(MollierPoint, sfp) rise instead, keeping the example's specific-fan-power
intent explicit as a named constant. The fans now run 16 -> 16.65 C and
30 -> 30.68 C, and both yield Pressure = 560 Pa = eta * SFP * 1000 exactly,
independent of inlet state, as the SFP relation requires.

The upstream factory defect is left for a separate SAM_Mollier change; this
uses only sound upstream APIs and stays correct once that is fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: prove fan, humidifier, twin-wheel, layout and liquid-loop behaviour

40 new tests replacing NotNull-only coverage with numerical and topological
proof. Every value is either hand-derived or independently re-derived in the
test; no assertion was relaxed to make a run green.

Query/FanPressureTests (6): dP is the exact inverse of PickupTemperature
(recovering the SFP from the pressure returns the original rise to 1e-9);
the SFP invariant dP == eta*sfp*1000 holds at two different inlet states,
proving rho and cp cancel; pressure scales LINEARLY with efficiency, which
fails outright if the efficiency is moved back to the denominator; NaN
guards for null/NaN/zero/negative efficiency and zero temperature rise.

Query/HumidifierPropertiesTests (10): spray setpoint equals End relative
humidity and is banded 50-75 %, excluding the old ~22 C dry-bulb value;
effectiveness and water flow re-derived from the humidity-ratio balance and
banded; steam duty is set and matches |m*dh| ~ 25 kW - the test first
asserts the process is NOT isothermal, proving the removed gate would have
suppressed it; components carry the derived values; edge cases: negative
humidity change clamps to 0, saturated start (zero denominator) yields no
effectiveness, NaN airflow leaves flows NaN but still sets the setpoint,
null process leaves everything NaN.

Query/HeatRecoveryEfficiencyTests (4): the query exactly inverts the
factory's design percentages (75/65 -> 0.75/0.65); sensible-only recovery
leaves latent NaN rather than 0; clamping holds at 1.0 for a 150% request.

Create/TwinWheelTopologyTests (6): the real TwinWheelExample fixture gives
exactly one AirSystem and exactly one shared exchanger despite both chains
carrying heat recovery; all four exchanger air connectors are connected
with In on 0/2 and Out on 1/3; efficiencies are 0.75/0.65 with latent type
HumidityRatio and survive a JSON round-trip; both fans carry 560 Pa, 0.7
efficiency and 2500/2300 l/s.

Create/TwoRowLayoutTests (6): exactly two rows at y = 0 and y = -0.8;
supply row steps left-to-right by exactly 1.0 per chain-consecutive
component; the extract row runs right-to-left along its airflow; the shared
exchanger is drawn once, on the supply row, reaching the extract row only
through connections; all air connections carry routed geometry and no
symbol is missing; a short supply-only chain falls back to a valid single
row.

Create/LiquidLoopTests (8): one boiler and one chiller on two named liquid
systems; every heating coil is wired liquid In 3 / Out 4 and every cooling
coil In 2 / Out 3; both loops are closed - the plant equipment has both
connectors connected and NONE left unconnected; each loop relates only its
own plant; the two default energy sources exist with the right types and
CO2 factors and the stored boiler/chiller reference them by name; the
template path injects no duplicate plant, loops or sources while still
merging the air side.

Test project also copies Plantroom-Only.json to the output for the template
test. 123 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: describe the bridge that actually exists

The docs described an implementation that in places never existed. Corrected
against the code, with every claim now backed by a passing test:

- Fan pressure: dP = eta*rho*cp*dT (efficiency multiplies), cp read from the
  inlet state via SpecificHeatCapacity_Air rather than a hardcoded 1010, and
  the inverse relationship to PickupTemperature stated. The old
  dP = rho*cp*dT/eta was wrong by 1/eta^2.
- Humidifier setpoint: end-state relative humidity [%] for BOTH spray and
  steam, with the Tas TPD rationale. The old "target humidity ratio" claim
  matched neither the code nor the consumer. Steam duty documented as
  unconditional, with the reason the isothermality gate was dead.
- Fan design flow documented in l/s with DesignFlowType = Value.
- Diagnostics: both docs now carry the same canonical 14-code table as
  ConversionDiagnostic.cs. Previously the plan's two tables disagreed with
  the code on ~8 of 14 codes and omitted MOLLIER-014, and an edge-case table
  contradicted the canonical severities and mapped null process/null chain
  onto MOLLIER-012; every code/severity mention across both docs was
  re-checked mechanically against the source. ConversionResult removed from
  the docs along with the code.
- Liquid systems: closed-loop wiring with connector indexes, plus the new
  default energy sources (names, CO2/cost values, EnergySourceName linkage)
  and the template path's skip-injection behaviour.
- One combined AirSystem carries both supply and extract, in the bridge doc,
  the plan and SupplyExtract.cs's own XML remarks - all three previously
  claimed two separate air systems.
- Section F pseudocode corrected (it still taught all three defects);
  Section P's historical prompts flagged as a pre-audit record rather than
  silently left to mislead.
- Counts corrected throughout: 123 tests across 17 files (was "74"/"30+
  across 11"), 24 files added / 15 modified (was 21/10), 12 commits.
- New Limitations section: structural-only validation with no Tas simulation
  or export exercised here, the MVRE comparison being type-level rather than
  count-for-count and why, and the upstream SAM_Mollier Create.FanProcess
  defect with its workaround.
- Unit XML docs added to SystemFan, SystemSprayHumidifier and
  SystemSteamHumidifier, whose setpoint units were previously undocumented -
  the ambiguity behind the wrong implementation.

The Section Q delivery-status table is deliberately untouched: it is updated
only after final verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: disambiguate crefs made ambiguous by the new diagnostics overloads

Adding the out-diagnostics overloads turned previously-unique
<see cref="SystemEnergyCentre"/> / <see cref="SystemPlantRoom"/> references
into ambiguous ones, adding 14 CS0419 warnings. Those crefs mean the return
TYPE, so qualify them as SAM.Core.Systems.*. Warning count returns to the
base branch's 21; the 19 pre-existing CS0419 locations are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: record delivery status against executed verification

Rewrites the Section Q status table now that the acceptance evidence exists.
The previous table marked all ten phases complete on the strength of code
existing: P3, P4 and P6 were in fact delivering wrong values, and P5 and P6
had no acceptance tests at all. Each phase now cites the tests that prove
it, and the verification table records results from clean worktrees with
SAM and SAM_Mollier at origin/sow/2026-Q3:

- build.ps1 Debug and Release: all 3 solutions, exit 0
- test.ps1 Debug: tests and TwinWheelExample.Verify both gated, exit 0
- dotnet test: 123 passed, 0 failed, 0 skipped, none returning early
- TwinWheelExample.Verify: 26/26 PASS lines with the display checks
  executed (the previously claimed 27/27 was never accurate)
- MVRE: 5 passed with the fixture provably loaded from the test output
- JSON round-trip: 10 passed
- 21 compiler warnings, identical to the base branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update mollier_processes_to_systems_IMPLEMENTATION_PLAN.md

* docs: correct the modified-file count and the SAMAnalyticalSystemResults note

The count was taken before this audit's own last production edits and went
stale by exactly the three files those edits touched: 18 files are modified
vs sow/2026-Q3, not 15. The three XML unit-doc files are now listed.

Also corrects the Section Q note calling SAMAnalyticalSystemResults.cs an
unrelated change. It is required: SAM PR #36 is merged on SAM sow/2026-Q3
and adds MinCompatibleVersion and ObsoleteSeverity to IGH_SAMComponent, so
without them SAM_Systems.sln does not compile against the target branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
SAM_Mollier PR #7 (fix/fan-process-pickup-temperature, merged into its
sow/2026-Q3) corrected Create.FanProcess(MollierPoint, double) to add the
pickup temperature to the inlet dry-bulb rather than assigning it as an
absolute temperature. The bridge-side workaround this branch's ancestor PR
added to work around that defect is no longer needed.

TwinWheelExample.cs: both example fans now call the standard
start.FanProcess(SpecificFanPower) directly; the private
FanProcessBySpecificFanPower helper and its defect-describing XML comment
are removed. SpecificFanPower stays 0.8. Fan-pressure derivation in
SAM_Systems (Query/FanPressureRise.cs) is untouched, as is all topology,
diagnostics, liquid-system and layout code.

docs/Mollier-Systems-Bridge.md: removed the Limitations bullet describing
the upstream defect and workaround (no longer true), and noted in
Status/TODO that the example uses the standard factory directly.

docs/mollier_processes_to_systems_IMPLEMENTATION_PLAN.md: updated the
Modified Files table's TwinWheelExample.cs row to describe the current
state rather than the removed workaround, and added a new Section S
recording the upstream fix and this follow-up (the existing Section R
historical record from 2026-07-17 is left as-is - it accurately describes
what was true when written).

Removed a test comment in FanPressureTests.cs that claimed
Create.FanProcess(start, sfp) could not be used to build test fixtures and
pointed at the now-removed workaround; the test's own logic is unchanged.

Both example fans still produce Pressure = 560 Pa, OverallEfficiency = 0.7
(= eta * SFP * 1000 = 0.7 * 0.8 * 1000), confirmed by
TwinWheel_Fans_HavePressureAndEfficiency in TwinWheelTopologyTests.cs.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Iteration 0 step 5b. SAM_Systems joins the workstream as the fourth repo.

CapabilityIndex.JSON sits beside the ten shipped templates and says what
each can do, so choosing a system reads about forty bits instead of the
twelve megabytes the templates weigh between them. Entries are keyed on the
existing SystemTemplate identity - no new system or equipment identity, and
no MVHR, because MVRE already is SAM's heat-recovery ventilation. Only the
ventilation part is stated: these are ventilation templates, and heating,
cooling, plant room and controls are chosen elsewhere.

Rank is the preference order among systems that all meet a requirement,
lower first. It lives here because it is a policy about this library rather
than something that follows from Approved Document F - which is exactly why
SAM.Analytical stopped inferring it. The order proposed is
simplest-provision-first and is Michal's to confirm.

Two findings from reading the templates, and both are recorded rather than
smoothed over:

- No shipped template models a summer bypass. SummerBypass is false for all
  ten, so a scenario requiring bypass is REFUSED until a template provides
  one. Iteration 2 needs that template. Declaring it true would credit a
  dwelling with mitigation the design does not have.
- Continuous ventilation and boost cannot be read off a template at all -
  nothing in the files marks either - so they are declared from the system
  type's meaning in SAM_SystemTypeLibrary.JSON. Each entry carries an
  EvidenceFromTemplate list naming the capabilities that WERE established
  from the file, and the test asserts the index says so honestly rather than
  presenting a declaration as a measurement.

Heat recovery IS in the files, and the conformance test checks every claim
against them: a template recovers heat exactly when it holds an exchanger
with a sensible efficiency. It also re-establishes the fact the whole
MVRE-is-MVHR decision rests on - one exchanger, 0.7 sensible, 0 latent, no
recirculation component - and that MV is the same system without it.

Resolution to a concrete SystemEnergyCentre matches on the ventilation
identity and refuses on ambiguity or a missing file, never a nearest match.
It is the one point at which a template is opened at runtime, and it happens
once, after the choice, for the system chosen.

The conformance test reads the repository's own resources rather than a
copy. The first version walked up to the first folder with a matching path
and found build_tests/net8.0/, where the Mollier tests copy two templates -
it would have conformance-checked the index against a two-file copy and
passed a library eight templates short. It now identifies the repository
root.

9 tests. SAM_Systems Mollier suite still 123 passed.

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review fixes on c6a441d. Two of these were confidently wrong answers, not
robustness polish.

A missing or mistyped Rank now refuses the whole index. It used to default
to 0, with a comment claiming that showed up as a tie and would be refused -
which was simply wrong: ONE missing rank is a unique 0, it sorts FIRST, and
the entry somebody forgot to rank becomes the preferred answer. The review
changed VAV's "Rank" to "rank" - one letter - and every dwelling requirement
then selected a commercial variable-air-volume unit in place of a dwelling
extract fan, silently.

A ventilation identity that is present but is not usable text also refuses
the index. An absent one still means "not a ventilation template" and is
skipped, which is what Plantroom-Only is; treating a broken one the same way
made a system quietly vanish from the library.

Identities are read as text and rebuilt through SystemTemplate's property
setters, which strip spaces. Its JSON path assigns fields raw, so an index
entry reading "M V" was offered as a selectable system and then resolved to
nothing, because a caller's constructor-built "M V" is "MV".

A Resource is a file name and nothing else - anything with a separator or a
parent reference is refused rather than combined, so an index cannot reach
outside the directory it ships in.

The "no template is opened" promise is now proved structurally instead of by
a stopwatch: the index is copied ALONE into an empty directory and the whole
selection still gives the same answers for every requirement Part F can
produce. The timing version was machine-dependent, a single long pause could
flip it, and it covered one requirement. The new test also asserts that
resolution DOES fail without the template, so it cannot pass by reading
nothing.

SummerBypass was declared evidence-backed and never checked. It is now: no
template may hold a bypass token other than BypassFactor, a cooling-coil
parameter that would false-positive a naive search. HeatRecovery is checked
for an efficiency ABOVE ZERO rather than merely for the key existing - an
exchanger set to 0 would have passed while claiming recovery - and MVRE is
asserted to hold exactly one exchanger.

MechanicalSupply added to the index for the SAM.Analytical vocabulary
change: false for NV, EOL, EOC and UV, true for the five that supply air.
Application is documentation only, so a caller reading CapableSystems can
tell that CAV, VAV and DISP are commercial air-handling types. The Rank
rationale is corrected to what the numbers actually are -
domestic-before-commercial, unconditioned last - not simplest-first, which
CAV, DISP and UV all contradicted.

SAM.Analytical.Systems.Tests is now in SAM_Systems.sln, so CI compiles it.
CI still runs no tests in this repo; recorded as a follow-up.

20 tests. Mollier suite still 123. Solution builds clean under VS MSBuild.

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 5c. Application was documentation, and Rank was doing a job it should
never have had.

A commercial air-handling type is not a worse answer for a dwelling - it is
not an answer - so it is now filtered out in SystemCapabilityDescriptors
before anything reaches SAM.Analytical. A Domestic request is offered the
Domestic and Any entries and nothing else. Previously CAV, VAV and DISP were
reported as SUITABLE for an Approved Document F requirement and only their
ranks kept them out of the answer, so one edited number could have put a
variable-air-volume unit in a flat. A test now inverts every rank in the
index so commercial sorts first, confirms that premise, and asserts a
dwelling still never sees one.

SystemApplication lives here, not in SAM.Analytical. Which of THESE
templates is a dwelling system is a fact about this repository's resources,
on the same footing as their capabilities; the analytical layer never learns
the words "domestic" or "commercial". A missing or unrecognised Application
refuses the whole index - defaulting it to eligible would offer an
unclassified template to a dwelling, defaulting it to ineligible would make
a system quietly vanish.

Rank is now recorded as what it is: declared library policy, not derived and
not a claim of engineering minimality. The domestic order is marked
PROVISIONAL and unconfirmed rather than justified - it makes selection
deterministic, nothing more. Commercial ranks no longer keep anything out of
a dwelling answer, so they order commercial-to-commercial selection only.

The commercial Boost values are recorded as unverified declarations. An
earlier revision justified CAV and DISP boost = false from "constant volume"
and "displacement regime", which is inferring a capability from a label
rather than knowing it. The values stay false - false refuses, true would
silently mis-assess - but the index now carries UnverifiedDeclarations and a
test asserts the name-derived justifications are gone.

CI executes the conformance suite instead of merely compiling it. It is the
only thing that checks the index against the shipped templates, and a
drifted index is otherwise invisible until it selects the wrong system.

27 tests. Mollier suite still 123. Solution builds clean under VS MSBuild.

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four of these were confidently wrong, not robustness polish.

The new CI test step silently restamped three shipped assemblies to
1.0.0.0. SAM.Analytical.Systems, SAM.Core.Systems and SAM.Geometry.Systems
all write to the same build\ folder the artifact step publishes, and
dotnet test without /p:SAMVersion recompiled them at the local-dev fallback
and overwrote the CI-stamped DLLs - so the uploaded artifact would have
shipped version-inconsistent assemblies and quietly defeated the
AssemblyInfo-versioning programme for this repo. Verified fixed: the test
build now leaves 2026.3.999.0 in place where it previously left 1.0.0.0.

VAV declared Boost true while listing Boost as unverified, contradicting
the index's own note and the previous commit message, both of which said
unverified values stay false. "Variable Air Volume" -> can vary -> can
boost is the same inference from a label that CAV and DISP were corrected
for, so the capability is no longer credited. A test now asserts every
capability named in an UnverifiedDeclarations list is false, which is what
would have caught it.

A request for SystemApplication.Any returned only the entries literally
marked Any - in this library the single template that can never be
selected - so a caller meaning "I will take anything" got every dwelling
refused with a message about missing capabilities rather than about the
filter. As a request, Any and Undefined both now mean no constraint; the
enum's two roles are documented as the asymmetry they are.

Enum.TryParse accepts numeric text and comma-separated name lists, and
because the members number Domestic=1, Commercial=2, Any=3, both "3" and a
well-meaning "Domestic,Commercial" parsed to Any - the most permissive
value there is - with Enum.IsDefined unable to object because the result is
defined. Applications are now matched against the three legal names,
ordinally.

The systemApplication parameter is no longer optional. Defaulting it to
Undefined made the commercial-inclusive call the one you get by writing
nothing, with a comment saying Part F must pass Domestic - a comment
enforcing exactly what the parameter existed to stop being a comment.

The index's guarantee is scoped honestly. It claimed a commercial template
cannot appear in a dwelling answer whatever the ranks say; that is true of
capability selection and NOT of the repository, because the older
Query.DefaultSystemEnergyCentres / Create.SystemEnergyCentre path derives a
template from a file NAME and matches it against a space's internal
condition, consulting neither the index nor Application. Four Grasshopper
components reach it and nothing in step 5 changed it. Recorded as a
follow-up and pinned by a test rather than left to be discovered.

Also: a non-commercial entry carrying UnverifiedDeclarations was forbidden
by a comment and now by an assertion; the description check no longer pins
one exact English phrase; the rank-inversion test guards the "!" marker it
depends on; ranks need only be distinct within an application, which is the
only set they order; the Sorted helper's de-duplication is documented and
backed by count assertions; and a failing test no longer costs the build
artifacts.

34 tests. Mollier suite still 123. Solution builds clean under VS MSBuild.

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`SystemCapabilityDescriptors` applied eligibility with a `continue` as soon as
it had read an entry's `Application`, which skipped everything below it: the
ventilation text check, `SystemTemplate.IsValid`, and the `Rank` refusal. So a
broken entry refused the whole index only when the caller happened to be asking
for that entry's application, and the same half-edited catalogue was accepted
for one application and rejected for another.

The entry this reopens the hole on is the one the reader's own `Rank` comment
records as the original defect: a case typo on VAV's `"Rank"`. VAV is
`Commercial`, so after eligibility became a filter that typo refused an `Any` or
`Commercial` request and was silently dropped from a `Domestic` one - and
Approved Document F selection is exactly the `Domestic` caller. The existing
`MalformedIndex_ProducesNoDescriptors` breaks EOL, a domestic entry, so it went
on passing.

Eligibility now filters the survivors instead: every entry is parsed and
validated first, and `IsEligible` is the last thing before `result.Add`. An
ineligible entry is therefore proved sound before being left out.

Reported by chatgpt-codex-connector on SAM_Systems#14.

Tests: `BrokenEntryOutsideTheRequestedApplication_StillRefusesTheIndex` breaks
VAV five ways and asserts all four request kinds refuse - all five cases fail
without the reordering. `ValidatingEveryEntry_DoesNotChangeWhatIsOffered` is the
other half, pinning that an intact index still answers 6 domestic / 4 commercial,
so the fix was not bought by leaking commercial templates into a dwelling answer.
40/40 SAM.Analytical.Systems.Tests and 123/123 Mollier tests pass in Release.

Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generated by Michal Dengusiak and CodeClaude

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
michaldengusiak and others added 27 commits September 24, 2026 14:33
docs: record merged SAM#123 manufacturer-guidance work in PROJECT_PROGRESS
Cooling rule ExchangerThenCoil (60-120 l/s: HX 87.96-81.36 %, DX drop
9.265-7.705 K, fan rise 0.3-1.1 K, 13 degC floor), bypass extract 19 degC,
cooling airflow range 60-120 l/s with an 80 l/s stated default; guidance
settings resolve the dwelling figure, then the default, then the midpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…9-24

feat(parto): Nuaire entry follows the 24 Sep 2026 reply
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: record the merged Nuaire reply work
A stated GuidanceTemplate (MVRE) makes the guidance settings partial: named
units are the product's MVRE + DX arrangement from the imported prototype,
every other unit ordinary MV, all in one plant room sharing one set of plant
collections. Every dwelling zone is now DisplacementVentilation = false (owner
decision; SAM#129). A guidance-settings overload operates at SAM's cooling
airflow rule. The legacy all-guidance call is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-2026-09-27

feat(parto): cooled and uncooled units in one materialisation (PR3B-2)
…a PR branch

Owner convention (28 Sep 2026): code + tests + evidence -> final PR CI ->
merge -> PROJECT_PROGRESS.md closeout (with the merge SHA) as a direct
docs-only commit on the base branch. While a PR is open, its status and
evidence live in the PR record document and PR description. Unrelated
stream entries are never modified. Same wording in SAM, SAM_Systems,
SAM_Tas, SAM_UI and SAM_Deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…upply fan (tests not yet run)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-09-29

fix(systems): place the guidance DX cooling coil between exchanger and supply fan
…48542d)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…system scope

SAM decides which ventilation systems a Part O Systems-route run assesses
(Query.PartOSystemsMaterialisationScope). SAM_Systems had no way to be told:
D2 resolved every system of the cluster (unit-less NV 1 / UV 1 refused), and
D10's duty cross-check summed every system naming a unit model-wide.

- New overload Create.MechanicalVentilation(..., spaces, guids_VentilationSystem).
  Null = the legacy call (the 4-parameter method delegates to it, unchanged).
  Stated: only those systems reach D2 and D10; a system not stated is never
  processed, so a malformed one cannot refuse. Empty, Guid.Empty or a guid that
  is not a ventilation system of the model refuses; repeats count once.
- Every refusal of a stated system is unchanged, with the same text.
- Identities unchanged: scoped on the whole model == unscoped on SAM's working copy.

Tests: MechanicalVentilationSystemScopeTests (34, production-shaped
AddMechanicalSystems scaffold NV 1 / UV 1 / MV 1 -> AHU1 beside two Part O
MVHR flats). 7 mutations killed. SAM.Analytical.Systems.Tests 303/303,
Mollier 123/123. Record: docs/PartO-SystemsBoundary-PR3.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…026-09-30

Part O PR-3: MechanicalVentilation honours a stated ventilation system scope
…893e71)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…om upstream contribution

Removes AGENTS.md, PROJECT_PROGRESS.md and internal evidence/.claude artefacts that are SAM-BIM workflow records, not upstream project documentation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PR1: Use selected Part O cooling control room
…ranch

Internal progress record; not part of the upstream contribution. It remains in sow/2026-Q3 history and will be restored in sow/2026-Q4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Drop the AI implementation-prompt and repository-workflow sections and signature line from the Mollier plan docs, reword a reviewer-name comment, neutralise a placeholder string in the shipped CapabilityIndex JSON comment text. No code change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@michaldengusiak
michaldengusiak merged commit e42d93a into HoareLea:master Oct 6, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants