Device Designarchiwum projektu „100 kampanii”

Wariant v011

{'name': 'MODEL-TDD', 'D1': 'maintainer (top session) + ticket workers (custom subagent type ticket-worker)', 'D2': 'the repository + its check results (CI) are the communication; tickets are files used as briefs; change notes back', 'D3': 'tests first -> physics model -> design-space exploration -> generated artefacts', 'D4': 'by ticket / module, with disjoint write sets', 'D5': 'executable acceptance check per requirement (status assigned by the engine) + consistency by construction (documents rendered from params, model outputs and check results; verify detects hand edits and stale renders)', 'D6': 'model optimum within the checks; topology by rule (knock-outs, priority-weighted score), ties by unit cost at 1000', 'D7': 'Agent-tool subagents (tickets, <= 4 in parallel when write sets are disjoint); SendMessage for continuations', 'D8': '3 topologies (2 only with a recorded reason) x continuous parameter sweeps and 2-D grids'}

Rodzic
brak (osobnik startowy)
Operator
scratch
Utworzony
30 października 2026, 03:55

Co się zmieniło

MODEL-TDD: the design is a code repository. Before any design, every requirement id of INPUT becomes an executable check (design/checks/req_<ID>.py) whose status is assigned by a shared check engine from the requirement's target and threshold, never by its author; every design number lives once in design/params/*.yaml with unit and origin; physics and cost are pure Python functions of the parameters; three topologies are compared by running all checks on each and sweeping their design variables (1-D sweeps and 2-D design-space maps), and the choice follows a fixed, robustness-tested rule. The top session is the maintainer: it writes tickets, ticket-worker subagents (<= 4 parallel, disjoint write sets) implement them, the maintainer merges through scoped gates and renders every deliverable (01/02, REPORT.md, compliance.csv, manifest.json, cost files, standalone calculation bundles) from Jinja2 templates with make, so numbers cannot disagree between documents and every result is reproducible offline.

Wyniki

Ten wariant nie ma jeszcze przebiegów.

Pełna treść wariantu

MAIN.md (251 wierszy)
# MODEL-TDD — the design is a repository: requirement checks first, one home per number, every document generated

## 1. The method
You are the **maintainer** of a design repository, `design/`, that turns `INPUT/brief.md` and
`INPUT/requirements.md` into a complete hardware product design with no human in the loop. Four rules:
1. **Tests first.** Every requirement id becomes an executable check `design/checks/req_<ID>.py` before any
   design exists. The check computes a value; the check engine (`design/ddlib.py`) assigns PASS / PARTIAL / FAIL
   from the requirement's target and threshold, so no agent can grade its own work.
2. **One home per number.** Every design number lives once, in `design/params/*.yaml`, with unit and origin;
   targets and test conditions live in `design/requirements/*.yaml`.
3. **Physics as code.** Performance, size, mass, power and cost are Python functions of the parameters
   (`design/model/`). Concepts are three topologies (parameter sets + model modules) compared by running every
   check on each and sweeping their design variables; the choice follows a fixed rule.
4. **Documents are generated.** `make` renders every deliverable from parameters, model and check results, so
   numbers cannot disagree between documents and `02-concept/calc/` recomputes every result offline.

Ticket workers (Claude Code subagents of type `ticket-worker`) each make one well-specified change; you write
tickets, review, merge, decide by rule and render. Why this shape: agents are strongest at code, executable
checks stop unsupported claims, file-disjoint tickets run in parallel without conflicts, and generation gives
consistency by construction.

## 2. First steps (T+0 to T+6)
1. Run `date -Iseconds`. Concept tier: target T+60 min, hard stop T+2 h. Full tier: plan in §7.
2. Read `INPUT/brief.md`, `INPUT/requirements.md`, `INPUT/TOOLCHAIN.md`, `method/HYGIENE.md` (binding for you and
   for every agent you start; each ticket tells the worker to read it), `method/POLICY.md`, and the handbook
   `.claude/skills/dd-hw-handbook/SKILL.md` (read the file; it defines the quality bar of every deliverable).
3. Create the repository. Shell hooks on this machine may refuse broad git staging (`-A`, `.`) and destructive
   git commands; use exactly these shapes, from the run directory:
   ```
   cp -r method/repo-template design
   git -C design init -q
   git -C design add -- Makefile README.md LOG.md ddlib.py .gitignore checks docs model params tools
   git -C design commit -q -m "repository template" -- Makefile README.md LOG.md ddlib.py .gitignore checks docs model params tools
   ```
4. Read `design/README.md`: the conventions every agent follows (formats, check pattern, model rules, helpers).
5. `make -C design reqs` imports the requirements into `design/requirements/<NNN>-<group>.yaml` and prints each id
   with its parsed sense, target, threshold and unit: compare them with INPUT (a comma before exactly three digits is
   read as a thousands separator); it must count as many ids as the package checker. What it could not parse
   (`argued` where a number was meant) goes to the requirement tickets, not fixed by hand now. Then
   `make -C design scaffold` (a check stub per id, an evidence stub per argued id) and commit both:
   `git -C design add -- requirements checks evidence` and
   `git -C design commit -q -m "requirements and check stubs" -- requirements checks evidence` (drop `evidence` from
   both when scaffold wrote no evidence stub: git refuses a path that holds no file).
6. Fill `meta.product` and `meta.currency` in `design/params/common.yaml`; check the business defaults there
   against INPUT. Write start time, tier and deadlines into `design/LOG.md` (your state file: keep its ticket
   table, decisions and next step current, so the run can resume from files alone). `mkdir -p deliverables`.

Run every command from the run directory: `make -C design <target>`, `ddpy design/<script>`.

## 3. Roles and write rights
- **You, the maintainer (top session)**: the only writer of `design/params/common.yaml`, `design/docs/*.j2`,
  `design/docs/map.yaml`, `design/LOG.md`, `design/blocked.yaml`, the tickets, the parts assigned to you, the git
  history, and — only through `make -C design docs | checkpoint | concept | full` — of everything in
  `deliverables/`, including `REPORT.md` (subagents cannot write files named REPORT*). Never hand-edit a
  deliverable: fix its source and re-render (`make -C design verify` detects hand edits).
- **ticket-worker subagents**: one ticket each; write only the ticket's write set; no git, no rendering, no
  `deliverables/` (except a source folder a full-tier ticket names), no files named REPORT*, SUMMARY*,
  FINDINGS* or ANALYSIS*. Their rules: `method/roles/ticket-worker.md`.
- At most **4 workers at once**, and only with **disjoint write sets**. No Oivo sub-sessions and no Workflow
  scripts: tickets are small file-isolated changes and subagents report straight back to you.
- Never message any session outside this run; there is nobody to ask.

## 4. Communication: the repository is the channel
- **Ticket** = `design/tickets/T<nn>-<slug>.md` written from `method/templates/ticket.md` (goal, context, write
  set, steps, done-when commands, time box). Add a row to the LOG ticket table.
- **Launch** — several calls in ONE message run in parallel; each returns at once with an agent id, and the
  worker's final message (its change note) arrives later as a notification. SendMessage and TaskStop are deferred
  tools: load them once at the start with ToolSearch `select:SendMessage,TaskStop`.
  `Agent({description: "T02 checks bundle B", subagent_type: "ticket-worker", prompt: "Run directory: <absolute run dir>. Your ticket: design/tickets/T02-checks-b.md. Read method/roles/ticket-worker.md, then the ticket, and do it."})`
- **Continue** a finished worker with its context (for follow-up work on the same files):
  `SendMessage({to: "<agent id>", message: "Ticket T09 continues your work: read design/tickets/T09-explore-a.md and do it."})`
- **Worker → you**: the files of its write set, `design/tickets/T<nn>.note.md`, and the same note as its final
  message. Workers never talk to each other.
- **CI**: `design/build/check-results.json`, written only by your full `make -C design check`, is the shared truth
  about the design. Nobody edits files in `design/build/`.
- **Merge** each returned ticket at once (about 2 minutes):
  1. Read the note. `git -C design status --porcelain -uall`: every changed path must be in this ticket's write set
     or in the write set of a ticket still running.
  2. Run the ticket's done-when commands (scoped: `ddpy design/checks/run_all.py --only '<ids>'`,
     `ddpy design/tools/lint.py --scope '<ids or paths>'`) and read the diff of what matters
     (`git -C design diff -- <paths>`; new files: read them).
  3. Pass → stage exactly this ticket's paths as that status lists them (a path that does not exist makes git
     refuse the whole command): `git -C design add -- <paths>` and
     `git -C design commit -q -m "T<nn> <title>" -- <paths>`; LOG row: merged + short sha.
  4. Fail → revert: `git -C design restore -- <its modified tracked paths>` and `rm -f -- <its untracked files>`
     (`??` lines); give the failure output once to the same worker by SendMessage; if it fails again, do it yourself.
  Run the full `make -C design lint check` only at a wave boundary, when no worker is running (files in flight
  are half-written).

## 5. Concept tier: work order and time boxes
At every boundary below: `date`, a LOG update (time, merged tickets, check counts) and a checkpoint render.

**Wave 0, T+0–6 (you):** §2. Split the requirement group files into three bundles of similar id count and assign
each derived-requirement area of handbook §2 except safety (T08 covers it) to exactly one bundle. Write T01–T04
and launch them in one message.
- **T01–T03 requirements and checks** (one bundle each, box 12 min). Write set: the bundle's
  `requirements/<NNN>-*.yaml`, `requirements/9<n>-derived-<bundle>.yaml`, `checks/req_<id>.py` and
  `evidence/<id>.md` for those ids. Do: fill `interpretation`, `consequence` and `conditions` (with origin) of every
  given requirement and correct parse errors against INPUT; refine each scaffolded check — numeric ones as
  `need(P, "<quantity>", <condition>=R.cond(...))` calls that define what the model must compute, feature decisions
  as tests of `P.features`; argued ones decided by documented design decisions where possible (the
  `(target_met, threshold_met)` tuple), else `Unknown` with its evidence file (a template: quote numbers with `req()`
  and `p()`). Add derived requirements (DR-<AREA>-<nn>, with parents and origin) for the assigned areas, at most
  two per area and only where the area adds a verifiable constraint (an area that needs none:
  `areas_not_applicable` with the reason), then `ddpy design/tools/scaffold.py checks --only 'DR-(<AREAS>)-'` and
  refine those stubs. Checks run before any topology exists (on the shared parameters): UNKNOWN is expected.
- **T04 research** (box 14 min, web). Write set: `research/benchmark.yaml`, `research/parts.yaml`,
  `docs/parts/benchmark.md.j2`, `docs/parts/compliance.md.j2`. Do: at least five comparable products in the schema of
  README "Research data" (specs that matter for the requirements, price with date and URL, weak points users
  report); price anchors and candidate key components with dated prices; the regulatory map (EU directives and
  harmonised standards, FCC rules) with sources and the likely conformity route.

**While wave 1 runs (T+6–18, you):** write `docs/parts/product.md.j2`, `functions.md.j2` and `physics.md.j2` (the
physics map: CRITICAL / RELEVANT / MINOR domains, governing relations, dominant parameters, the model module
that will cover each). Draft three topologies that differ in **working principle**, not cosmetics:
`design/params/A.yaml`, `B.yaml`, `C.yaml` from `method/templates/topology.yaml` (name, principle, features, the
design variables with first values, ranges and origins). Two topologies only if no third principle exists —
write why in LOG and the decision part. Merge wave 1 as it arrives; at the boundary run
`make -C design lint check`, `make -C design needs` (the model functions the checks call: the interface wave 2
implements) and `make -C design checkpoint` (first deliverables on disk).

**Wave 2, T+18–35:** launch in one message:
- **T05–T07 model one topology each** (box 15 min). Write set: `model/topo_<T>.py`, `params/<T>.yaml`,
  `bom/<T>.yaml`, `docs/parts/topo_<T>.md.j2`. Do: `ddpy design/tools/scaffold.py model <T>` (a stub for every
  function the checks need and the key figures `length`, `width`, `height`, `mass`, `power`), then replace the stubs
  in order of the priority of the checks that use them, each with a hand estimate, plus selftests; real parameter
  values (sources found fast, else ASSUMPTION with range); a rough BOM of the major items with prices at
  1/100/1000 (dated where read) and other costs (labour, packaging, tooling);
  the part: how it works, a Mermaid sketch, main risks and hazards. Goal: every check computable for T or an
  explicit Unknown with its reason; the same coverage on every topology (a gap can decide the ranking, G3).
- **T08 safety** (box 15 min). Write set: `docs/parts/safety.md.j2`, `requirements/95-derived-safety.yaml`, checks
  and evidence for those DR-SAF ids. Do: top hazards of the three topologies (ISO 12100 manner), inherently safe
  design first, protections that act without firmware, derived safety requirements with checks
  (`ddpy design/tools/scaffold.py checks --only 'DR-SAF-'`, then refine).
Physics shared by several topologies goes into one shared module (`model/<name>.py`) owned by exactly one of
these tickets — name it in that ticket; the others reach it through `need(P, ...)`, never by copying it.
Meanwhile write `docs/parts/conflicts.md.j2`. Merge; at the boundary `make -C design lint check` and
`make -C design checkpoint` (its `decide` gives the provisional leader).

**Wave 3, T+35–45: exploration and decision.** Continue T05–T07 by SendMessage with tickets T09–T11 (box 8 min;
write set `explore/<T>_*.py`, `params/<T>.yaml`, `model/topo_<T>.py`): first implement what `make needs` now
lists as missing or `stub` for T (`scaffold.py model <T>` adds stubs for new needs, e.g. of the safety checks),
then one sweep of each of the two most influential design variables and one 2-D grid of them against the decisive
checks (`dd.sweep`, `dd.grid`, README); set the best values with origin CALCULATION citing the script; report the
score before and after. Meanwhile draft
`architecture.md.j2` for the provisional leader and `decisions.md.j2` from LOG. Merge; then
`make -C design check decide`. Set `meta.chosen` to the winner (POLICY §2: override only with a recorded reason).

**Finish, T+45–55 (you):** complete the shipped skeletons `docs/parts/decision.md.j2` (why the winner won and
each loser lost), `risks.md.j2` and `open.md.j2`: keep their generated lines, replace each `[[WRITE …]]`; finish
`architecture.md.j2` for the winner (Mermaid block diagram, subsystems make-or-buy, critical parts with candidates,
interfaces) and `decisions.md.j2`; write `design/blocked.yaml`. Run
`make -C design concept` (lint → check → decide → calcs → docs → verify → package); fix each error at its
source and re-run until it passes; commit. Then finish (§10).

**What the concept tier delivers — all generated, nothing typed:**
- `01-requirements/requirements.md`: each given requirement with statement, interpretation, consequence,
  conditions, acceptance rule, the source of its check and its result for every topology; derived
  requirements; conflicts; the assumptions register with the computed effect of each assumption's range.
  `compliance.csv` is generated from the check results of the chosen topology (met = PASS, partly = PARTIAL,
  not_met = FAIL, not_addressed = NOT RUN or BLOCKED: nothing is claimed without a computed check; documents use
  the RUN CONTRACT status words, NOT RUN for the engine's UNKNOWN). `source/` holds the data.
- `02-concept/concept.md` in the handbook's order: the three topologies as parameter tables + model outputs +
  check counts, the sweep and design-space plots, the ranking with its robustness tests, the chosen architecture
  with rough BOM, key estimates with origins, safety, compliance route, cost and retail price, risks, decisions,
  open issues. Beside it: `check-results.md/.json`, `decision.json`, `plots/`, `explore/`, `physics-map.md`,
  `benchmark.md`, `model.md`, `params/`, `bom/`, `evidence/`, and `calc/model_<T>.py` + `.results.json`.
- `REPORT.md`, `manifest.json` and `09-bom-cost/cost.json`.

**Stop rule (concept tier):** when `make -C design concept` passes, finish (§10). Start no CAD, drawings,
electronics, firmware or renders. No ticket starts after T+50.

## 6. Gates
- **G1 merge** (each ticket): changes inside the write set; scoped checks without ERROR/MISSING; scoped lint clean.
- **G2 wave** (`make -C design lint check`): every INPUT id has a requirement entry and a check; parameters carry
  unit, origin and note; no status literals in checks; no typed quantities in templates; model selftests pass;
  no ERROR or MISSING.
- **G3 decision** (`make -C design decide`): ranking by rule; robustness tests re-rank all topologies for weights
  ±20 %, every ASSUMPTION fact at both range ends (on every topology that uses it) and model coverage (an id NOT RUN
  for one topology but computed for another); every reason the winner could change is named in the decision text.
- **G4 render** (`make -C design verify`): deliverables equal the last render, the render is not stale, no
  `[[WRITE …]]`, `[[MISSING …]]` or raw template left.
- **G5 package** (`make -C design package` = `method/gates/dd_check.py --run-scripts`): the RUN CONTRACT layout and
  formats; the calculation bundles reproduce every status.
- **Full tier** adds the artefact gates inside `make full`: `make cad` runs `step-info --interference` and
  `gltf-validate`; KiCad ERC and DRC (in `06-electronics/build.py`), `pio run` and native `pio test` (`make fw`),
  simulations' known-answer cases.

## 7. Full tier: path to every deliverable
**F1, T+0–1.5 h:** sections 2–5 with longer boxes (requirement tickets 25 min, research 30 min, model tickets
30 min, exploration 15 min); render with `make -C design checkpoint TIER=full` (never `make concept` at this
tier). F1 ends with `meta.chosen` set and the decision parts written.
**F2, T+1.5–6 h: detailed design of the chosen topology**, tickets of ≤ 45 min, at most four at once with
disjoint write sets, numbers only from the `make -C design params` exports (README "Full tier"):
- **CAD** → `03-cad/`: `deliverables/03-cad/src/build.py` (CadQuery or build123d) writes one STEP per designed part,
  `assembly.step`, `parts.csv`, a mass and bounding-box report, and `11-renders/assembly.glb` (`step2glb`); then
  `length`/`width`/`height`/`mass` in the model read that report (`ddlib.read_deliv`), so checks see the geometry.
- **Drawings** → `04-drawings/`: `03-cad/src/drawings.py`, every manufactured part plus the assembly with balloons
  that match the `bom.csv` items.
- **Simulation** (only where a margin is below 20 % or an assumption flips a status) → `design/sim/<stem>.py` with a
  known-answer case, read by the model via `ddlib.sim_result`; exported to `05-calculations/` with the bundle.
- **Electronics** → `06-electronics/`: `build.py` generates the KiCad 9 project from `params.json` (the headless
  recipe named in INPUT/TOOLCHAIN.md), runs ERC and DRC (reports kept), writes fabrication outputs; the power
  budget becomes model functions.
- **Firmware** → `07-firmware/`: PlatformIO project for the schematic's MCU using `include/design_params.h`;
  native host tests of the state machine and interlocks, written first.
- **BOM** → `09-bom-cost/`: real purchasable parts in `bom/<chosen>.yaml` (offline catalogue first, dated prices);
  `make -C design docs TIER=full` generates `bom.csv`, `cost.json`, `cost.md`.
- **Safety and certification** → `08-safety-certification/`: templates `docs/safety-*.md.j2` that you add to
  `docs/map.yaml` (hazard analysis, directives and standards with sources, conformity route, lab tests,
  pre-compliance plan, cost and time).
- **Prototype and test plan** → `10-prototype-test-plan/`: template with the build plan, bring-up, and one test per
  requirement id and per safety requirement (a table generated from `reqs` and the checks).
- **Renders** → `11-renders/`: `render.py`, a Blender script that `make renders` runs with `blender -b` (from the
  GLB): product, exploded and user-interface views; scale checked against a known dimension.
- `05-calculations/` comes from `make -C design calcs TIER=full` (bundle of the chosen topology plus simulations).
After each merge wave: `make -C design check` and `make -C design checkpoint TIER=full`.
**F3, T+6–8 h:** `make -C design full` until it passes; a remaining failure is fixed at its source or written into
`blocked.yaml` and `docs/parts/open.md.j2` with its reason. Soft stop T+9 h (POLICY §5).

## 8. Failure handling
- Custom type `ticket-worker` unavailable: use `subagent_type: "general-purpose"` with the same prompt (the
  worker reads its role file anyway).
- Agent tool unusable: do the tickets yourself, one after the other, with two topologies and shorter boxes; log it.
- A worker silent at twice its box: inspect its write set, merge what passes G1, stop it with
  `TaskStop({task_id: "<agent id>"})` (load TaskStop with ToolSearch `select:TaskStop`), and finish the rest yourself.
- A worker breaks the gates: revert (§4), one retry by SendMessage with the failure output, then do it yourself.
- A requirement no model or decision can settle: `Unknown` (NOT RUN in the documents) with an argued evidence file;
  never a made-up value.
- A tool or library fails: three attempts with new information each, then a labelled fallback (an analytical
  model instead of a simulation) and an entry in `design/blocked.yaml`.
- git refused by a hook: do not try other spellings; before each ticket copy its write set to
  `design/build/snap/T<nn>/` as the revert point and continue without commits.
- Out of time (concept): from T+50 launch nothing; at T+60 run `make -k -C design concept` (every step despite failing
  gates), fix what `dd_check` reports as ERROR, list the rest in `docs/parts/open.md.j2`, re-render, finish by T+75.

## 9. What you must not do
Hand-type a design number into a template, part or deliverable; mark a requirement met without a computed check;
weaken a requirement, condition or check to pass it; start detailed design at the concept tier; leave a worker
running when you finish.

## 10. Finishing
1. `make -C design concept` (concept tier) or `make -C design full` (full tier) passes; its last step runs the
   package checker `method/gates/dd_check.py`. Out of time: the `make -k` fallback of §8 has rendered everything
   and the remaining failures are listed as open.
2. Read `deliverables/REPORT.md` once against `01-requirements/compliance.csv` and `02-concept/concept.md`; fix
   any wrong sentence in its part and re-render.
3. Every worker has returned or been stopped; nothing runs in the background. Final commit; LOG: "done".
4. End your last message with the typed JSON final report your Oivo contract defines and `GUI_STATE` with
   `state: done` (RUN CONTRACT §6).
agents/ticket-worker.md (19 wierszy)
---
name: ticket-worker
description: Executes one MODEL-TDD ticket in the design repository design/ of a hardware design run - requirement checks, physics or cost model modules, parameter sets, sweeps, research data, prose parts, or full-tier artefact generators. Use it for every ticket the maintainer writes; the prompt names the ticket file.
model: opus
---
You are a ticket worker in a MODEL-TDD hardware design run: you make one well-specified change to the design
repository `design/` and report it. Your prompt names the run directory and your ticket file.

First read, in this order: `method/roles/ticket-worker.md` (your rules and the change-note format),
`method/HYGIENE.md` (binding ground rules), `design/README.md` (repository conventions), then your ticket.

Essentials, in case you read nothing else: write only your ticket's write set; every number lives in
`design/params/`, `design/requirements/` or `design/bom/` with unit, origin and note; checks compute values and
never assert a status; never run `make check|explore|decide|scaffold|docs|concept|full|checkpoint` (only the
scoped commands of your ticket) or edit `deliverables/` unless your ticket says so; never write files named
REPORT*, SUMMARY*, FINDINGS* or ANALYSIS*; nobody answers questions, so decide with `method/POLICY.md`; stay
inside your time box; end with your change note (also written to `design/tickets/T<nn>.note.md`) as your final
message.
files/method/HYGIENE.md (24 wierszy)
## Ground rules for every agent in this run

1. **Every number has a home and an origin.** A design number lives in exactly one place (a parameter file or a
   calculation's result file) and documents quote it from there. Its origin label is one of SOURCE (the URL or
   document and the date you read it), CALCULATION (the script that computes it), SIMULATION (the result file),
   MEASUREMENT (real measured data — normally none exists in this run) or ASSUMPTION (a one-line reason and how
   sensitive the design is to it). A number with no origin is deleted, not guessed.
2. **Nothing is invented.** Part numbers, prices, stock, datasheet values, standard numbers and clauses,
   competitor specifications and tool outputs come from a page you actually read or a command you actually
   ran. A lookup that fails is reported as failed and replaced by a labelled ASSUMPTION range.
3. **Estimate before you compute.** Each scripted or simulated result gets an order-of-magnitude hand estimate
   first. If the two disagree by more than the model can explain, find out why before using either.
4. **Prove a route before trusting it.** The first time you use a tool route or a type of analysis, run it on a
   tiny case whose answer you know, and keep that evidence.
5. **Status words mean what they say.** PASS, PARTIAL, FAIL, BLOCKED, NOT RUN. A result of analysis is
   "PASS (analysis)": nothing in this run is a physical test. A FAIL stays FAIL until a new check passes.
6. **The product claims only what the design shows.** Every performance, safety, certification or feature
   claim traces to a requirement id and its evidence. A claim without evidence is removed or marked as an
   unproven goal. We are designing a real product for honest crowdfunding, not a pitch.
7. **Nobody will answer you.** You decide. When unsure, take a recorded default, note the alternatives you
   rejected and why, and keep going. Never stop to wait for a person.
8. **Time is scored.** Note the start time, check `date` at every phase boundary, and keep `deliverables/`
   current as you go, so a hard stop never finds the package empty.
files/method/POLICY.md (71 wierszy)
# Decision policy (MODEL-TDD) — what replaces the human's answers

Nobody answers questions in this run. When a choice is open, apply this file, record the choice in
`design/LOG.md` (and in `docs/parts/decisions.md.j2` when it shapes the product), and continue.

## 1. Priorities, highest first
1. Safety of users and bystanders: hazards removed by design first, then protections that work without firmware,
   then warnings.
2. Legal sale in the target markets (EU and US unless INPUT says otherwise).
3. Honest claims: a status comes from a computed check; what no model or documented decision settles is NOT RUN
   (the engine's UNKNOWN), keeps its argument and claims nothing (compliance `not_addressed`).
4. Requirement thresholds, in order of priority (the weight in INPUT); then targets, in the same order.
5. Unit cost at 1000 units, then at 100 units, then the prototype cost.
6. Run time and cost.

## 2. Decision rules
- **Topology choice is mechanical** (`make -C design decide`, rule in `design/tools/decide.py`): fewest knock-outs
  (a FAIL of a priority ≥ 4 requirement), then the priority-weighted status score (PASS 1, PARTIAL 0.5, else 0);
  a score within 0.02 of the leader is a tie, broken by the lower unit cost at 1000 units. The maintainer sets
  `meta.chosen` to the winner. Override only for a safety or legal reason, or a risk the model cannot represent;
  then write the ranking, the reason and what would have to be true for the winner to be right into
  `docs/parts/decision.md.j2`.
- **Design values = model optimum within the checks**: the values that maximise the weighted score; among scores
  within 0.02 the cheaper at 1000 units; then the higher score; then the larger weighted mean margin (`ddlib.rank`).
  Set them with origin CALCULATION and the script.
- **Equal model coverage before deciding**: a requirement NOT RUN for one topology but computed for another is a
  modelling gap, not a design result. Close it (wave 3) or name it in the decision text; `make decide` marks the
  result not robust when such a gap could change the winner.
- **Requirement conflicts**: higher priority wins; equal priority → safety, then cost. Record each conflict in
  `docs/parts/conflicts.md.j2` with both requirement ids.
- **Missing information**: an ASSUMPTION parameter with a one-line reason and a `range` that covers the plausible
  values; `make decide` computes its effect. Prefer a SOURCE found in two minutes of searching over an assumption.
- **A FAIL is a result**. Never weaken a target, threshold, condition or check to turn FAIL into PASS: change the
  design, or report the FAIL with its margin and what would fix it.
- **Not robust** (`decision.json` robust = false): keep the rule's winner, name each reason
  (`not_robust_because`: a flipping weight, assumption or coverage gap) in the decision text, and make retiring it
  the first open issue.

## 3. Defaults for taste and unstated requirements
- Low voltage inside: when the power level allows, a certified external power supply; mains inside only when the
  power needs it, then with a certified mains inlet, fuse and protective earth or double insulation.
- Pre-certified radio modules only; never a discrete RF design.
- MCU from a family the toolchain builds offline (see INPUT/TOOLCHAIN.md); hardware protections for every hazard
  (thermal cut-off, current limit, interlock) so firmware is never the only protection.
- Physical controls on the product for the core functions; an app may add but never be required.
- Form: a simple, robust housing with rounded edges, matte neutral colours (black or dark grey for professional
  equipment, white or light grey for home use), clear labels; no decorative features that cost money or tooling.
- Metric ISO fasteners, one tool type, wear parts replaceable without special tools.
- Plastics with a flammability rating suited to nearby heat and live parts; restricted substances avoided.
- Prototype by 3D printing or CNC; 1000 units by the process that minimises unit cost with tooling amortised.
- Currency of INPUT; SI units, geometry in mm; documents in English.
- Business defaults (in `params/common.yaml`, ASSUMPTION with ranges): gross margin 0.40, channel fees 0.10
  (crowdfunding platform plus payment), VAT 0.20 — replace by the market's rates when INPUT names the market.

## 4. Fidelity
- Concept: first-order physics (energy, mass, force and heat balances, lumped thermal networks, datasheet curves).
  A simulation only if it can change the topology decision.
- Full tier: deepen a model only where a margin is below 20 % of its threshold or an assumption flips a status;
  known-answer test first, three resolutions with the trend, balances checked.

## 5. Time plan and stop rules
- Concept tier (target 60 min, hard stop 2 h): render a checkpoint at every wave boundary; final
  `make -C design concept` by T+55; stop at the first passing run. No ticket starts after T+50. Still failing at
  T+60: `make -k -C design concept` (every step despite failing gates), fix what the package gate reports as ERROR,
  list the rest as open issues, finish by T+75.
- Full tier (plan 6–9 h): F1 concept by T+1.5 h, F2 detailed design by T+6 h, F3 integration by T+8 h; soft stop
  T+9 h: from then on only fixes that turn an ERROR of the package gate into a pass.
- Cut order when behind: sweep resolution → number of explored variables → depth of non-decisive models
  (UNKNOWN with argument) → prose length. Never cut: one check per requirement id, origins, generated documents,
  the gates, honesty.
files/method/gates/dd_check.py (431 wierszy)
#!/usr/bin/env python3
"""dd_check.py - check deliverables/ against the RUN CONTRACT layout (contract v1.x).

Usage (from the run directory):
    python3 method/gates/dd_check.py --tier concept|full [--run-scripts] [--json]

Checks the formats the RUN CONTRACT fixes (compliance.csv, manifest.json, bom.csv, cost.json, the
folder layout of the tier, evidence paths) and prints ERROR / WARN lines. Exit code 0 = no errors.
It checks form, not engineering quality. Standard library only.
"""
import argparse
import csv
import io
import json
import os
import re
import shutil
import subprocess
import sys

COMPLIANCE_HEADER = ["req_id", "status", "value", "unit", "evidence", "note"]
COMPLIANCE_STATUS = {"met", "partly", "not_met", "not_addressed"}
BOM_HEADER = ["item", "qty", "description", "manufacturer", "mpn", "supplier", "supplier_part", "url",
              "unit_price_1", "unit_price_100", "unit_price_1000", "currency", "notes"]
HEADING_ID = re.compile(r"^#{1,6}\s+([A-Z][A-Z0-9]{1,7}-[A-Z0-9]{1,7}-\d{1,4})\b")
LOOSE_ID = re.compile(r"\b([A-Z][A-Z0-9]{1,7}-[A-Z]{2,7}-\d{2,4})\b")
FULL_DIRS = ["03-cad", "04-drawings", "05-calculations", "06-electronics", "07-firmware",
             "08-safety-certification", "09-bom-cost", "10-prototype-test-plan", "11-renders"]


class Report:
    def __init__(self):
        self.errors, self.warnings, self.notes = [], [], []

    def error(self, msg):
        self.errors.append(msg)

    def warn(self, msg):
        self.warnings.append(msg)

    def note(self, msg):
        self.notes.append(msg)


def requirement_ids(path):
    """Ids of INPUT/requirements.md: headings first, loose tokens as a fallback."""
    if not os.path.isfile(path):
        return []
    with open(path, encoding="utf-8", errors="replace") as fh:
        text = fh.read()
    ids = []
    for line in text.splitlines():
        m = HEADING_ID.match(line.strip())
        if m and m.group(1) not in ids:
            ids.append(m.group(1))
    if not ids:
        for m in LOOSE_ID.finditer(text):
            if m.group(1) not in ids:
                ids.append(m.group(1))
    return ids


def nonempty_files(folder, exts=None):
    out = []
    for root, _dirs, files in os.walk(folder):
        for name in files:
            p = os.path.join(root, name)
            if os.path.getsize(p) == 0:
                continue
            if exts and not name.lower().endswith(tuple(exts)):
                continue
            out.append(p)
    return out


def split_paths(field):
    parts = re.split(r"[;|\n]+|\s+(?=[\w./-]+/)", field or "")
    return [p.strip().strip("`'\"") for p in parts if p.strip()]


def evidence_exists(deliv, ref):
    ref = ref.split("#", 1)[0].split("::", 1)[0].strip()
    if not ref:
        return True
    if ref.startswith("deliverables/"):
        ref = ref[len("deliverables/"):]
    ref = ref.lstrip("./")
    m = re.match(r"^(.*?)(:\d+(-\d+)?)$", ref)
    if m:
        ref = m.group(1)
    return os.path.exists(os.path.join(deliv, ref))


def check_compliance(deliv, ids, rep):
    path = os.path.join(deliv, "01-requirements", "compliance.csv")
    if not os.path.isfile(path):
        rep.error("01-requirements/compliance.csv missing")
        return
    raw = open(path, encoding="utf-8-sig", errors="replace").read()
    rows = list(csv.reader(io.StringIO(raw)))
    if not rows:
        rep.error("compliance.csv is empty")
        return
    header = [h.strip() for h in rows[0]]
    if header != COMPLIANCE_HEADER:
        rep.error(f"compliance.csv header is {header}, expected {COMPLIANCE_HEADER}")
        return
    seen = {}
    for n, row in enumerate(rows[1:], start=2):
        if not any(c.strip() for c in row):
            continue
        if len(row) != len(COMPLIANCE_HEADER):
            rep.error(f"compliance.csv line {n}: {len(row)} fields, expected {len(COMPLIANCE_HEADER)} (quote fields with commas)")
            continue
        rid, status, _value, _unit, evidence, _note = [c.strip() for c in row]
        if rid in seen:
            rep.error(f"compliance.csv line {n}: duplicate id {rid} (first at line {seen[rid]})")
        seen[rid] = n
        if status not in COMPLIANCE_STATUS:
            rep.error(f"compliance.csv line {n}: status '{status}' not in {sorted(COMPLIANCE_STATUS)}")
        if status != "not_addressed" and not evidence:
            rep.error(f"compliance.csv line {n} ({rid}): status {status} without evidence path")
        for ref in split_paths(evidence):
            if not evidence_exists(deliv, ref):
                rep.error(f"compliance.csv line {n} ({rid}): evidence path not found: {ref}")
    if ids:
        missing = [i for i in ids if i not in seen]
        extra = [i for i in seen if i not in ids]
        if missing:
            rep.error(f"compliance.csv lacks {len(missing)} requirement id(s): {', '.join(missing)}")
        if extra:
            rep.warn(f"compliance.csv has ids not in INPUT/requirements.md: {', '.join(extra)}")
        rep.note(f"compliance.csv: {len(seen)} rows for {len(ids)} requirement ids")
    else:
        rep.warn("no requirement ids found in INPUT/requirements.md; row coverage not checked")


def check_ids_in_requirements(deliv, ids, rep):
    folder = os.path.join(deliv, "01-requirements")
    texts = ""
    for p in nonempty_files(folder, (".md", ".yaml", ".yml", ".txt")):
        texts += open(p, encoding="utf-8", errors="replace").read() + "\n"
    if not texts:
        rep.error("01-requirements/ has no requirements document (.md/.yaml) besides compliance.csv")
        return
    absent = [i for i in ids if i not in texts]
    if absent:
        rep.warn(f"requirement ids not mentioned in the 01-requirements documents: {', '.join(absent)}")


def num_or_null(x):
    return x is None or isinstance(x, (int, float)) and not isinstance(x, bool)


def check_manifest(deliv, tier, rep):
    path = os.path.join(deliv, "manifest.json")
    if not os.path.isfile(path):
        rep.error("manifest.json missing")
        return
    try:
        man = json.load(open(path, encoding="utf-8"))
    except Exception as exc:  # noqa: BLE001
        rep.error(f"manifest.json does not parse: {exc}")
        return
    if man.get("tier") != tier:
        rep.error(f"manifest.json tier is {man.get('tier')!r}, expected {tier!r}")
    files = man.get("files")
    if not isinstance(files, list) or not files:
        rep.error("manifest.json files[] missing or empty")
    else:
        listed = set()
        for f in files:
            if not isinstance(f, dict) or not {"path", "kind", "desc"} <= set(f):
                rep.error(f"manifest.json files[] entry lacks path/kind/desc: {f}")
                continue
            listed.add(f["path"].lstrip("./"))
            if not evidence_exists(deliv, f["path"]):
                rep.error(f"manifest.json lists a file that does not exist: {f['path']}")
        actual = set()
        for p in nonempty_files(deliv):
            rel = os.path.relpath(p, deliv)
            if rel != "manifest.json":
                actual.add(rel)
        unlisted = sorted(a for a in actual if a not in listed and not any(a.startswith(l.rstrip("/") + "/") for l in listed))
        if unlisted:
            rep.warn(f"{len(unlisted)} deliverable file(s) not listed in manifest.json, e.g. {unlisted[:5]}")
    kf = man.get("key_figures")
    if not isinstance(kf, dict):
        rep.error("manifest.json key_figures missing")
    else:
        for key in ("dimensions_mm", "mass_kg", "power_w", "unit_cost", "retail_price"):
            if key not in kf:
                rep.error(f"manifest.json key_figures.{key} missing (use null when unknown)")
        dims = kf.get("dimensions_mm")
        if dims is not None and not (isinstance(dims, list) and len(dims) == 3 and all(num_or_null(d) for d in dims)):
            rep.error("key_figures.dimensions_mm must be [L, W, H] numbers or null")
        for key in ("mass_kg", "power_w"):
            if key in kf and not num_or_null(kf[key]):
                rep.error(f"key_figures.{key} must be a number or null")
        uc = kf.get("unit_cost")
        if uc is not None:
            if not isinstance(uc, dict) or not {"currency", "q1", "q100", "q1000"} <= set(uc):
                rep.error("key_figures.unit_cost needs currency, q1, q100, q1000")
            elif not all(num_or_null(uc[q]) for q in ("q1", "q100", "q1000")):
                rep.error("key_figures.unit_cost q1/q100/q1000 must be numbers or null")
        rp = kf.get("retail_price")
        if rp is not None and (not isinstance(rp, dict) or not {"currency", "value"} <= set(rp) or not num_or_null(rp.get("value"))):
            rep.error("key_figures.retail_price needs currency and a numeric value (or null)")
    blocked = man.get("blocked")
    if not isinstance(blocked, list):
        rep.error("manifest.json blocked[] missing (use [] when nothing is blocked)")
    else:
        for b in blocked:
            if not isinstance(b, dict) or not {"what", "why"} <= set(b):
                rep.error(f"manifest.json blocked[] entry needs what and why: {b}")


def check_cost_json(deliv, rep, required):
    path = os.path.join(deliv, "09-bom-cost", "cost.json")
    if not os.path.isfile(path):
        if required:
            rep.error("09-bom-cost/cost.json missing")
        return None
    try:
        cost = json.load(open(path, encoding="utf-8"))
    except Exception as exc:  # noqa: BLE001
        rep.error(f"cost.json does not parse: {exc}")
        return None
    for key in ("currency", "bom", "unit_cost"):
        if key not in cost:
            rep.error(f"cost.json lacks {key}")
    for key in ("bom", "unit_cost"):
        block = cost.get(key)
        if isinstance(block, dict) and not all(num_or_null(block.get(q)) for q in ("q1", "q100", "q1000")):
            rep.error(f"cost.json {key} needs numeric q1, q100, q1000")
    other = cost.get("other", {})
    if not isinstance(other, dict):
        rep.error("cost.json other must be an object of {name: {q1, q100, q1000}}")
    else:
        try:
            for q in ("q1", "q100", "q1000"):
                total = float(cost["bom"][q]) + sum(float(v.get(q) or 0) for v in other.values())
                unit = float(cost["unit_cost"][q])
                if unit > 0 and abs(total - unit) / unit > 0.02:
                    rep.warn(f"cost.json unit_cost.{q}={unit} differs from bom+other={total:.2f} by >2 %")
        except (KeyError, TypeError, ValueError):
            pass
    return cost


def as_float(text):
    try:
        return float(str(text).replace(",", ".").strip())
    except ValueError:
        return None


def check_bom(deliv, cost, rep):
    path = os.path.join(deliv, "09-bom-cost", "bom.csv")
    if not os.path.isfile(path):
        rep.error("09-bom-cost/bom.csv missing")
        return
    rows = list(csv.reader(io.StringIO(open(path, encoding="utf-8-sig", errors="replace").read())))
    if not rows or [h.strip() for h in rows[0]] != BOM_HEADER:
        rep.error(f"bom.csv header must be exactly {','.join(BOM_HEADER)}")
        return
    totals = {"unit_price_1": 0.0, "unit_price_100": 0.0, "unit_price_1000": 0.0}
    currencies = set()
    lines = 0
    for n, row in enumerate(rows[1:], start=2):
        if not any(c.strip() for c in row):
            continue
        if len(row) != len(BOM_HEADER):
            rep.error(f"bom.csv line {n}: {len(row)} fields, expected {len(BOM_HEADER)}")
            continue
        rec = dict(zip(BOM_HEADER, [c.strip() for c in row]))
        lines += 1
        qty = as_float(rec["qty"])
        if qty is None:
            rep.error(f"bom.csv line {n}: qty '{rec['qty']}' is not a number")
            continue
        if not rec["mpn"] and not rec["supplier_part"]:
            rep.warn(f"bom.csv line {n}: neither mpn nor supplier_part ({rec['description'][:40]})")
        if rec["url"] and not rec["url"].startswith(("http://", "https://")):
            rep.warn(f"bom.csv line {n}: url is not http(s)")
        currencies.add(rec["currency"])
        for col in totals:
            price = as_float(rec[col])
            if price is None:
                rep.warn(f"bom.csv line {n}: {col} '{rec[col]}' is not a number")
            else:
                totals[col] += qty * price
    rep.note(f"bom.csv: {lines} lines; sums 1/100/1000 = {totals['unit_price_1']:.2f} / "
             f"{totals['unit_price_100']:.2f} / {totals['unit_price_1000']:.2f} {','.join(sorted(currencies))}")
    if len(currencies) > 1:
        rep.warn(f"bom.csv mixes currencies {sorted(currencies)}; totals not comparable")
    elif cost and isinstance(cost.get("bom"), dict):
        for q, col in (("q1", "unit_price_1"), ("q100", "unit_price_100"), ("q1000", "unit_price_1000")):
            ref = as_float(cost["bom"].get(q))
            if ref and abs(ref - totals[col]) / ref > 0.02:
                rep.warn(f"cost.json bom.{q}={ref} differs from the bom.csv sum {totals[col]:.2f} by >2 %")


def toolchain_python():
    """The toolchain Python the RUN CONTRACT names (ddpy), else the venv, else this interpreter."""
    for cand in (shutil.which("ddpy"), "/opt/dd/venv/bin/python"):
        if cand and os.path.exists(cand):
            return cand
    return sys.executable


def check_scripts(folder, rep, run):
    scripts = [p for p in nonempty_files(folder, (".py",))]
    for script in scripts:
        stem = os.path.splitext(script)[0]
        results = stem + ".results.json"
        if run:
            try:
                proc = subprocess.run([toolchain_python(), os.path.basename(script)], cwd=os.path.dirname(script),
                                      capture_output=True, text=True, timeout=300)
                if proc.returncode != 0:
                    rep.error(f"{os.path.relpath(script)} exits {proc.returncode}: {proc.stderr.strip()[-200:]}")
            except subprocess.TimeoutExpired:
                rep.error(f"{os.path.relpath(script)} runs longer than 5 minutes")
        if not os.path.isfile(results):
            rep.warn(f"{os.path.relpath(script)} has no {os.path.basename(results)}")
            continue
        try:
            data = json.load(open(results, encoding="utf-8"))
            bad = [k for k, v in data.items() if not (isinstance(v, dict) and "value" in v and "unit" in v)]
            if bad:
                rep.warn(f"{os.path.relpath(results)}: entries without value/unit: {bad[:5]}")
        except Exception as exc:  # noqa: BLE001
            rep.error(f"{os.path.relpath(results)} does not parse: {exc}")


def check_full(deliv, rep, run):
    for d in FULL_DIRS:
        if not nonempty_files(os.path.join(deliv, d)):
            rep.error(f"{d}/ missing or empty")
    cad = os.path.join(deliv, "03-cad")
    asm = os.path.join(cad, "assembly.step")
    if not os.path.isfile(asm):
        rep.error("03-cad/assembly.step missing")
    else:
        if not open(asm, "rb").read(64).lstrip().startswith(b"ISO-10303-21"):
            rep.error("03-cad/assembly.step is not a STEP file (no ISO-10303-21 header)")
    steps = [p for p in nonempty_files(cad, (".step", ".stp")) if not p.endswith("assembly.step")]
    if not steps:
        rep.error("03-cad/ has no per-part STEP files")
    if not nonempty_files(cad, (".py", ".fcstd")):
        rep.error("03-cad/ has no parametric source (.py or .FCStd)")
    if not nonempty_files(os.path.join(deliv, "04-drawings"), (".pdf", ".svg")):
        rep.error("04-drawings/ has no PDF or SVG drawing")
    check_scripts(os.path.join(deliv, "05-calculations"), rep, run)
    ele = os.path.join(deliv, "06-electronics")
    if not nonempty_files(ele, (".kicad_pro",)) or not nonempty_files(ele, (".kicad_sch",)):
        rep.error("06-electronics/ lacks a KiCad project (.kicad_pro + .kicad_sch)")
    names = " ".join(os.path.basename(p).lower() for p in nonempty_files(ele))
    if "erc" not in names:
        rep.warn("06-electronics/ has no file named like an ERC report")
    if nonempty_files(ele, (".kicad_pcb",)) and "drc" not in names:
        rep.warn("06-electronics/ has a board but no file named like a DRC report")
    if not os.path.isfile(os.path.join(deliv, "07-firmware", "platformio.ini")):
        rep.error("07-firmware/platformio.ini missing")
    cost = check_cost_json(deliv, rep, required=True)
    check_bom(deliv, cost, rep)
    if not os.path.isfile(os.path.join(deliv, "09-bom-cost", "cost.md")):
        rep.error("09-bom-cost/cost.md missing")
    ren = os.path.join(deliv, "11-renders")
    if not nonempty_files(ren, (".png",)):
        rep.error("11-renders/ has no PNG render")
    glb = os.path.join(ren, "assembly.glb")
    if not os.path.isfile(glb):
        rep.error("11-renders/assembly.glb missing")
    elif open(glb, "rb").read(4) != b"glTF":
        rep.error("11-renders/assembly.glb is not a binary glTF file")


def main():
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("--tier", choices=["concept", "full"], required=True)
    ap.add_argument("--run-dir", default=".", help="run directory (default: current directory)")
    ap.add_argument("--run-scripts", action="store_true", help="execute calculation scripts (≤5 min each)")
    ap.add_argument("--json", action="store_true", help="print a JSON summary as the last line")
    args = ap.parse_args()
    run_dir = os.path.abspath(args.run_dir)
    deliv = os.path.join(run_dir, "deliverables")
    rep = Report()
    if not os.path.isdir(deliv):
        rep.error("deliverables/ missing")
    else:
        ids = requirement_ids(os.path.join(run_dir, "INPUT", "requirements.md"))
        if not os.path.isdir(os.path.join(deliv, "01-requirements")):
            rep.error("01-requirements/ missing")
        else:
            check_compliance(deliv, ids, rep)
            check_ids_in_requirements(deliv, ids, rep)
        if not nonempty_files(os.path.join(deliv, "02-concept"), (".md",)):
            rep.error("02-concept/ has no markdown document")
        report = os.path.join(deliv, "REPORT.md")
        if not os.path.isfile(report):
            rep.error("REPORT.md missing")
        elif os.path.getsize(report) < 1500:
            rep.warn("REPORT.md is shorter than 1500 bytes")
        check_manifest(deliv, args.tier, rep)
        check_scripts(os.path.join(deliv, "02-concept"), rep, args.run_scripts)
        if args.tier == "concept":
            check_cost_json(deliv, rep, required=False)
            started = [d for d in FULL_DIRS if d != "09-bom-cost" and nonempty_files(os.path.join(deliv, d))]
            if started:
                rep.warn(f"concept tier: detailed-design folders present ({', '.join(started)}); they are not judged at this tier")
        else:
            check_full(deliv, rep, args.run_scripts)
    for msg in rep.notes:
        print(f"NOTE  {msg}")
    for msg in rep.warnings:
        print(f"WARN  {msg}")
    for msg in rep.errors:
        print(f"ERROR {msg}")
    print(f"dd_check: tier={args.tier} errors={len(rep.errors)} warnings={len(rep.warnings)} -> "
          f"{'PASS' if not rep.errors else 'FAIL'}")
    if args.json:
        print(json.dumps({"tier": args.tier, "pass": not rep.errors, "errors": rep.errors,
                          "warnings": rep.warnings, "notes": rep.notes}))
    return 0 if not rep.errors else 1


if __name__ == "__main__":
    sys.exit(main())
files/method/repo-template/.gitignore (3 wierszy)
build/
__pycache__/
files/method/repo-template/LOG.md (12 wierszy)
# Maintainer log — the state file (resume from files alone: this, git log, build/check-results.json)

Start: (date) · Tier: (concept|full) · Target / hard stop: (times)

## Tickets
| id | title | write set | agent id | launched | state | merge commit |
|---|---|---|---|---|---|---|

## Decisions (time — decision — alternatives rejected — why)

## Next step
files/method/repo-template/Makefile (37 wierszy)
# MODEL-TDD design repository. Run from the run directory:  make -C design <target>
# Maintainer only: docs, concept, full (they write deliverables/, including REPORT.md).
PY   ?= $(shell command -v ddpy 2>/dev/null || echo python3)
TIER ?= concept
D    := ../deliverables

.PHONY: reqs scaffold lint check needs explore decide calcs docs verify package checkpoint concept params cad drawings renders ecad fw full all

reqs:     ; $(PY) tools/req_import.py
scaffold: ; $(PY) tools/scaffold.py checks
lint:     ; $(PY) tools/lint.py
check:    ; $(PY) checks/run_all.py
needs:    ; $(PY) checks/run_all.py --needs
explore:  ; @set -e; for f in explore/*.py; do [ -e "$$f" ] || continue; echo "== $$f"; timeout 900 $(PY) $$f; done
decide:   ; $(PY) tools/decide.py
calcs:    ; $(PY) tools/export_calcs.py --tier $(TIER)
docs:     ; $(PY) tools/render.py --tier $(TIER)
verify:   ; $(PY) tools/lint.py --verify
package:  ; $(PY) ../method/gates/dd_check.py --run-dir .. --tier $(TIER) --run-scripts

# checkpoint: render what exists now, whatever the gates say (wave boundaries; keeps deliverables/ current)
checkpoint: ; -$(MAKE) -k check decide calcs docs TIER=$(TIER)
# concept tier: everything 01/02/REPORT/manifest needs, in order, stopping at the first failing gate
concept:  ; $(MAKE) lint check decide calcs docs verify package TIER=concept

# full tier: artefact generators written by tickets (see README "Full tier"), then the same chain
params:   ; $(PY) tools/export_params.py
cad:      params ; mkdir -p build && timeout 1800 $(PY) $(D)/03-cad/src/build.py \
	&& step-info --interference $(D)/03-cad/assembly.step > build/step-info.json \
	&& gltf-validate $(D)/11-renders/assembly.glb > build/gltf-validate.json
drawings: ; timeout 1800 $(PY) $(D)/03-cad/src/drawings.py
renders:  ; timeout 1800 blender -b --factory-startup --python-exit-code 1 -t 8 -P $(D)/11-renders/render.py
ecad:     params ; timeout 1800 $(PY) $(D)/06-electronics/build.py
fw:       params ; timeout 1200 pio run -d $(D)/07-firmware && timeout 1200 pio test -d $(D)/07-firmware -e native
full:     ; $(MAKE) lint cad drawings renders ecad fw check decide calcs docs verify package TIER=full
all: full
files/method/repo-template/README.md (161 wierszy)
# Design repository (MODEL-TDD) — conventions for every agent

The design is this repository. Requirements are executable checks; every design number lives once in
`params/`; physics and cost are Python functions of the parameters; every deliverable is generated from them.
Run every command from the run directory with the toolchain Python: `make -C design <target>`, `ddpy design/...`.

## Layout and write rights
| path | content | written by |
|---|---|---|
| `params/common.yaml` | `meta` (product, currency, chosen) + parameters shared by all topologies | maintainer |
| `params/<T>.yaml` | one topology (`A`, `B`, `C`): `topology` {id, name, principle}, `features`, `params` | its topology ticket |
| `requirements/*.yaml` | given (imported from INPUT) and derived requirements, one file per group | requirement tickets |
| `checks/req_<ID>.py` | one executable check per requirement id (stubs from `make scaffold`) | requirement tickets |
| `evidence/<ID>.md` | the argument of an argued requirement (a Jinja template, same number rule as `docs/`) | requirement tickets |
| `model/topo_<T>.py`, `model/<shared>.py` | physics and cost models | model tickets |
| `bom/<T>.yaml` | parts and other costs of topology T | topology / BOM tickets |
| `explore/<T>_*.py` | sweeps and design-space grids | explore tickets |
| `research/benchmark.yaml`, `research/parts.yaml` | comparable products, price anchors (SOURCE data; "Research data") | research ticket |
| `docs/*.j2`, `docs/map.yaml` | document templates and what render writes | maintainer |
| `docs/parts/<name>.md.j2` | prose sections (product, functions, physics, topo_<T>, conflicts, decision, architecture, safety, compliance, risks, decisions, open, benchmark, pricing); decision, risks and open ship as skeletons with `[[WRITE ...]]` markers | the ticket that names them |
| `tickets/` | `T<nn>-<slug>.md` tickets, `T<nn>.note.md` change notes | maintainer / worker |
| `blocked.yaml` | `blocked: [{what, why}]` for manifest.json | maintainer |
| `build/` | generated, never edited (check-results, decision, explore, plots, rendered) | make |

Only the maintainer runs `make check | checkpoint | docs | concept | full`, `tools/render.py`, git commits, and writes
`deliverables/`; workers run the scoped commands of their ticket.

## Parameters — `params/*.yaml`
```yaml
params:
  wall_thickness: {value: 2.0, unit: mm, kind: design, origin: ASSUMPTION, note: "printable and stiff enough; refine by FEM", range: [1.2, 3.0]}
  steel_density: {value: 7850, unit: kg/m^3, kind: fact, origin: SOURCE, note: "https://... read 2026-10-30"}
```
`value`, `unit` (pint syntax; `""` = plain number; `%`, `degC`, `EUR`, `dBA` work), `origin` ∈ SOURCE (note: reference +
date read) | CALCULATION (note: script, e.g. `explore/A_power.py best point`) | SIMULATION | MEASUREMENT | ASSUMPTION
(note: reason), `note`. `kind`: `design` (a variable you choose; give `range`) or `fact`. Every ASSUMPTION gets a
`range` so its sensitivity is computed (`make decide` moves every ASSUMPTION fact to both ends; a design variable's
range is its exploration range). Optional `label`, `c_name` (exported to firmware). A topology entry with the name of a
common one overrides it: lint warns unless its note says `override: <reason>`. Never use the names topology, info,
features, has, entry, names, with_.

## Requirements — `requirements/*.yaml`
`make reqs` imports INPUT/requirements.md. Each entry: `id`, `kind` (given | derived), `title`, `statement`,
`metric`, `sense` (`<=` | `>=` | `range` | `bool` | `argued`), `unit` (+ `unit_label`), `target`, `threshold`
(numbers in `unit`; `[lo, hi]` for range), `priority` (weight), `raw_target`/`raw_threshold` (as written),
`checked_by`, `conditions` ({name: {value, unit, origin, note}} — the exact verification condition the check uses),
`interpretation`, `consequence`. Derived: id `DR-<AREA>-<nn>`, `parents`, `rationale`, `verification`, `origin` of its
bounds (default ASSUMPTION); a handbook area that needs none goes into the file's top-level
`areas_not_applicable: {area: reason}`. Never weaken a target or threshold to make a check pass; fix an import error
only with a note in the change note.

## Checks — `checks/req_<ID>.py` (written before any model)
```python
"""<ID> <title>: what is computed and how it is accepted (one or two lines)."""
import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
from ddlib import need, Unknown, run_check


def compute(P, R):
    rise = need(P, "surface_temp_max", ambient=R.cond("ambient")) - R.cond("ambient")
    return rise          # a pint Quantity in any unit convertible to the requirement's unit


if __name__ == "__main__":
    run_check(__file__, compute)
```
- `compute(P, R)` returns a Quantity (numeric senses; also for `%` or `ppm`: a bare number is refused), a bool, or
  `(target_met, threshold_met)` for bool/argued requirements. The check engine (ddlib.verdict) converts units and sets
  PASS (target met), PARTIAL (threshold only), FAIL (threshold missed). A check never prints or returns a status and
  types no numbers: conditions come from `R.cond()`, values from `need(P, ...)` or `P.<param>`.
- Argued requirements are decided by documented design decisions where possible: return the tuple from
  `P.features` (each feature named and justified in the topology's part or the evidence file).
- Nothing can decide it yet: `raise Unknown("why; what would compute it", evidence="evidence/<ID>.md")` and write
  that file. The engine reports UNKNOWN; documents say NOT RUN and compliance `not_addressed` (nothing is claimed),
  so compute what can be computed. Never make a check pass by construction.
- `make scaffold` (maintainer, after `make reqs`) writes a stub per requirement without a check, and an evidence stub
  per argued one; `ddpy design/tools/scaffold.py checks --only '<regex>'` does the same for new derived ids.
- Run one: `ddpy design/checks/req_<ID>.py --topology A` (one JSON line). Run a scope:
  `ddpy design/checks/run_all.py --only '<regex>' [--topology A]`. Model functions the checks call: `make -C design needs`.
  Before any `params/<T>.yaml` exists both run on `params/common.yaml` alone (topology `-`): UNKNOWN is expected then.

## Model — `model/*.py`
- Pure functions `name(P, ...) -> Quantity` of the parameters; `need(P, "name")` resolves `model/topo_<T>.py`
  first, then shared modules (any other file name), then ddlib built-ins (`bom_cost_q1|q100|q1000`,
  `unit_cost_q1|q100|q1000`, `one_off_cost`, `retail_price`). Import helpers with `from ddlib import Q_, need, Unknown`.
- `ddpy design/tools/scaffold.py model <T>` writes a `@stub` for every function the checks need that nothing provides
  yet (and the key figures). A stub raises Unknown; any real function of the same name, also in a shared module,
  wins over it. Replace the stubs in priority order of the checks that use them.
- Each function: docstring with the governing relation and an order-of-magnitude hand estimate; each module: at
  least one `selftest_<x>()` known-answer test (a limiting case or a textbook value, asserted with a tolerance).
- Units everywhere (pint). Temperatures: `degC` is an offset unit — differences are `delta_degC` or `K`.
  Physical constants from `ddlib.ureg` (e.g. `ureg.standard_gravity`); materials and component data are params.
- `OUTPUTS = ["name", ...]` per module: key estimates shown in the documents (functions callable as `f(P)`).
  Reserved key figures (manifest.json): `length`, `width`, `height` (bounding box), `mass`, `power` (rated input).
- A missing parameter or model piece raises Unknown (reported, not hidden). Sweeps and `make decide` call each
  function hundreds of times: keep a call under ~10 ms (reduce fidelity, say so in the docstring). Full tier:
  geometry-derived values read the CAD reports (`ddlib.read_deliv("03-cad/<file>")`), simulations write
  `sim/<stem>.results.json` read via `ddlib.sim_result()`.

## BOM — `bom/<T>.yaml`
```yaml
currency: EUR
items:
  - {item: 1, qty: 1, description: "...", manufacturer: "", mpn: "", supplier: "", supplier_part: "", url: "", price: [q1, q100, q1000], origin: ASSUMPTION, notes: "why / date read"}
  - {item: 2, qty: "=fastener_count", ...}           # quantity taken from a parameter
other:                                           # per-unit costs besides parts, at 1 / 100 / 1000 units
  assembly_labour: {q1: 0, q100: 0, q1000: 0, origin: ASSUMPTION, notes: "..."}
  tooling: {one_off: 0, amortise_over: batch, at: [q1000], origin: ASSUMPTION, notes: "..."}
```
Prices in another currency: `currency:` on the line plus a parameter `fx_<FROM>_<TO>`. The full tier needs real
purchasable parts (offline catalogue first, see INPUT/TOOLCHAIN.md) with the date read in `notes`.

## Exploration — `explore/<T>_<what>.py`
```python
import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
import ddlib as dd
print(dd.sweep("A", "wall_thickness", checks=["<ID>", "<ID>"]))                  # values default to its range
print(dd.grid("A", "x_param", dd.span("A", "x_param", 15), "y_param", dd.span("A", "y_param", 15), checks=["<ID>"]))
```
Writes `build/explore-<tag>.json` and `build/plots/<tag>.png` (score map, threshold contours, current design, best
point) and returns the best point. Set the chosen values in `params/<T>.yaml` with origin CALCULATION and the
script in the note. Keep each script under ~2 min (grids ≤ 15 × 15).

## Research data — `research/`
```yaml
products:     # benchmark.yaml: five or more comparable products, each read on its page
  - {name: "...", maker: "...", price: "<amount> <currency> incl. VAT", specs: {<metric>: "<value> <unit>"}, weak_points: ["..."], url: "https://...", read: 2026-10-30}
```
`parts.yaml`: `parts: [{part, maker, mpn, price, currency, qty, url, read, note}]` (price anchors, key components).

## Documents — `docs/` and `evidence/`
Jinja2 templates rendered by `tools/render.py` (list in `docs/map.yaml`); `evidence/*.md` are templates too. Prose
sections are parts `docs/parts/<name>.md.j2`, included with `part("<name>")`. Quote numbers, never type them:
- `p("param"[, "T"])` → "900 W (ASSUMPTION)"; `o("output"[, "T"])` → "1.2 kW (CALCULATION model/topo_A.py:power)";
  `req("<ID>", "target" | "threshold" | "<condition>")` → "5 min (SOURCE INPUT/requirements.md)";
  `val("<ID>"[, "T"], label=True)` → the check's value with its label. In tables that have an origin column pass
  `label=False` to `p`, `o`, `req`.
- `c("<ID>"[, "T"]).status` (PASS, PARTIAL, FAIL, NOT RUN, BLOCKED — the RUN CONTRACT words), `cell("<ID>")`,
  `bounds(R["<ID>"])`, `fmt(x, unit)`; data: `R` (requirements by id), `topos`, `reqs`, `results`, `outputs`,
  `decision`, `cost`, `assumptions`; `versus("<T>")` and `tight(n)` feed the decision and risk skeletons.
`make lint` rejects a typed quantity (number + unit) outside `{{ }}`: a design number becomes a parameter. Only an
external fact (a standard's test level, a competitor's figure, a number of INPUT) may stay typed, on a line that
carries `SOURCE` and its reference (URL, standard number or `INPUT/...`). Mermaid diagrams are welcome (same rule).

## Make targets and gates
`reqs` import requirements · `scaffold` check and evidence stubs · `checkpoint` (maintainer) check, decide, calcs, docs despite failing gates · `lint` repository gate · `check` all checks × all topologies → `build/check-results.json`
(exit 1 on ERROR/MISSING/failed selftest) · `needs` · `explore` · `decide` → `build/decision.json` · `calcs`
standalone bundles (`02-concept/calc/` or `05-calculations/`) · `docs` render (maintainer) · `verify` deliverables
equal the last render and are not stale · `package` the RUN CONTRACT checker · `concept` = lint check decide calcs
docs verify package · `full` = lint cad drawings renders ecad fw check decide calcs docs verify package.

## Full tier — artefact generators (numbers only from `make params` exports)
`make params` writes `deliverables/03-cad/params.json`, `06-electronics/params.json`,
`07-firmware/include/design_params.h`. Authored sources live in their deliverable folder: CAD
`03-cad/src/build.py` (STEP per part + `assembly.step`, `parts.csv`, a mass/bounding-box report, `11-renders/assembly.glb`;
`make cad` then runs `step-info --interference` and `gltf-validate`) and `03-cad/src/drawings.py` (→ `04-drawings/`);
`11-renders/render.py` (a Blender script: `make renders` runs it with `blender -b`, from the GLB); `06-electronics/build.py`
(KiCad project, ERC/DRC reports, fabrication outputs); `07-firmware/` (PlatformIO, `native` host tests);
simulations `design/sim/<stem>.py` (≤ 5 min, write `<stem>.results.json`). Each generator reads the exported
parameters and never types a design number.
files/method/repo-template/checks/run_all.py (105 wierszy)
#!/usr/bin/env python3
"""CI of the design repository: every requirement check on every topology.

  ddpy design/checks/run_all.py                         all checks x all topologies -> build/check-results.json
  ddpy design/checks/run_all.py --only 'PRF|SIZ' --topology A [--why]   scoped run: prints, writes nothing
  ddpy design/checks/run_all.py --needs                 model functions the checks call, and who provides them

Exit 1 when a check in scope is MISSING or ERROR or a model selftest in scope fails (all modules for the full run;
topo_<T> and shared modules with --topology; none with --only alone). FAIL is a design result, not an error.
Before any params/<T>.yaml exists, checks run on params/common.yaml alone (topology "-"; nothing is written).
"""
import argparse
import datetime
import json
import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

ap = argparse.ArgumentParser()
ap.add_argument("--topology", nargs="*", help="topology ids (default: all)")
ap.add_argument("--only", help="regex on requirement ids")
ap.add_argument("--why", action="store_true", help="print the reason of every UNKNOWN")
ap.add_argument("--needs", action="store_true")
a = ap.parse_args()
tops = a.topology or sorted(dd.topologies()) or [dd.PLACEHOLDER]
if tops == [dd.PLACEHOLDER]:
    print("no params/<T>.yaml yet: checks run on params/common.yaml alone (expect UNKNOWN for model quantities)")

if a.needs:
    pat = re.compile(r"need\(\s*P\s*,\s*[\"']([A-Za-z_]\w*)[\"']")
    used = {}
    for rel in dd.files("checks/req_*.py"):
        for name in pat.findall(dd.read(rel)):
            used.setdefault(name, []).append(Path(rel).stem[4:])
    print(f"{'model function':<28} " + " ".join(f"{t:>4}" for t in tops) + "  used by   (stub = scaffold stub, still Unknown)")
    for name in sorted(used):
        have = [("stub" if getattr(f, "stub", False) else "yes") if f else " - " for f in (dd.find(t, name)[0] for t in tops)]
        print(f"{name:<28} " + " ".join(f"{h:>4}" for h in have) + "  " + ", ".join(sorted(set(used[name]))))
    sys.exit(0)

ids = [i for i in dd.ordered_ids() if not a.only or re.search(a.only, i)]
bad = 0
tests = dd.selftests()


def in_scope(name):
    stem = Path(name.split(":")[0]).stem
    return not a.only if not a.topology else not stem.startswith("topo_") or stem[5:] in tops


for name, ok, msg in tests:
    if not ok:
        bad += in_scope(name)
        print(f"SELFTEST FAIL {name}: {msg}" + ("" if in_scope(name) else "  (outside this scope: not counted)"))
results, outputs = {}, {}
for T in tops:
    P = dd.load_params(T)
    results[T] = dd.evaluate_all(P, ids)
    if not a.only:
        outputs[T] = dd.outputs(P)


def cell(r):
    v = r["value"]
    txt = dd.fmt(v, r["unit"]) if isinstance(v, (int, float)) else (v or "")
    return f"{r['status']:<7} {txt}"[:30]


problems = []
for rid in ids:
    print(f"{rid:<14} " + " | ".join(f"{T}: {cell(results[T][rid]):<30}" for T in tops))
    for T in tops:
        r = results[T][rid]
        if r["status"] in ("ERROR", "MISSING"):
            bad += 1
            problems.append(f"  {rid} [{T}] {r['status']}: {r['reason']}")
        elif r["status"] == "UNKNOWN" and (a.why or a.only):
            problems.append(f"  {rid} [{T}] UNKNOWN: {r['reason']}")
if problems:
    print("\n".join(["", "Problems / reasons:"] + problems))
print()
summary = {}
for T in tops:
    s = dd.score(results[T])
    summary[T] = s
    counts = " ".join(f"{k}={v}" for k, v in sorted(s["counts"].items()))
    print(f"topology {T}: {counts}  weighted score {s['score']:.3f}  knock-outs {len(s['knockouts'])}")
print(f"selftests: {sum(ok for _, ok, _ in tests)}/{len(tests)} ok")

if not a.only and not a.topology and tops != [dd.PLACEHOLDER]:
    dd.BUILD.mkdir(parents=True, exist_ok=True)
    data = {"generated": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
            "inputs_sha": dd.sources_hash(), "input_ids": dd.input_ids(),
            "selftests": [{"name": n, "ok": ok, "message": m} for n, ok, m in tests],
            "topologies": {T: {"name": dd.topologies()[T].get("topology", {}).get("name", T),
                               "results": results[T], "summary": summary[T], "outputs": outputs[T]} for T in tops}}
    (dd.BUILD / "check-results.json").write_text(json.dumps(data, indent=1, default=str))
    print("wrote build/check-results.json")
elif not a.only and not a.topology:
    print("nothing written: build/check-results.json needs at least one params/<T>.yaml")
sys.exit(1 if bad else 0)
files/method/repo-template/ddlib.py (717 wierszy)
"""ddlib - shared library of the MODEL-TDD design repository (read design/README.md first): units (pint),
parameters, requirements, the check engine (need, evaluate, verdict), cost built-ins, scoring and ranking,
exploration (sweep, grid). Repository scripts import it; calculation bundles embed it (use_bundle)."""
import ast
import fnmatch
import hashlib
import importlib
import json
import math
import numbers
import re
import signal
import sys
import threading
import types
from pathlib import Path

import pint
import yaml

ROOT = Path(__file__).resolve().parent          # design/
RUN, BUILD = ROOT.parent, ROOT / "build"
DELIV = RUN / "deliverables"
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

ORIGINS = ("SOURCE", "CALCULATION", "SIMULATION", "MEASUREMENT", "ASSUMPTION")
SENSES = ("<=", ">=", "range", "bool", "argued")
QS, NQ = ("q1", "q100", "q1000"), {"q1": 1, "q100": 100, "q1000": 1000}
STATUS_SCORE = {"PASS": 1.0, "PARTIAL": 0.5}          # FAIL, UNKNOWN, ERROR, MISSING score 0
COMPUTED = ("PASS", "PARTIAL", "FAIL")
SHOWN = {"UNKNOWN": "NOT RUN", "MISSING": "NOT RUN", "ERROR": "BLOCKED"}   # engine status -> RUN CONTRACT word
KEY_FIGURES = ("length", "width", "height", "mass", "power")
PLACEHOLDER = "-"                                       # topology id used before any params/<T>.yaml exists
_trace = threading.local()                              # model functions and parameters a check touches
HEADING_ID = re.compile(r"^#{1,6}\s+([A-Z][A-Z0-9]{1,7}-[A-Z0-9]{1,7}-\d{1,4})\b")   # as method/gates/dd_check.py
LOOSE_ID = re.compile(r"\b([A-Z][A-Z0-9]{1,7}-[A-Z]{2,7}-\d{2,4})\b")

ureg = pint.UnitRegistry(autoconvert_offset_to_baseunit=True)
for _c in ("EUR", "USD", "PLN", "GBP", "CHF", "CNY"):
    ureg.define(f"{_c} = [currency_{_c.lower()}]")     # currencies never mix silently
ureg.define("dBA = [sound_level_a]")                     # bookkeeping unit: do the log arithmetic yourself
Q_ = ureg.Quantity
UNIT_ALIASES = {"°C": "degC", "℃": "degC", "dB(A)": "dBA", "°": "deg", "Ω": "ohm", "µm": "um", "l": "L"}
LOG_UNITS = {"dB", "dBm", "dBi", "dBV", "dBW", "dBc"}


class Unknown(Exception):
    """Raise when a value cannot be computed (yet); the reason is reported, evidence = optional path."""

    def __init__(self, reason, evidence=""):
        super().__init__(reason)
        self.evidence = evidence


def shown(status):
    """Status word in deliverables: UNKNOWN and MISSING read NOT RUN, ERROR reads BLOCKED (RUN CONTRACT words)."""
    return SHOWN.get(status, status)


def stub(fn):
    """Marks a scaffold stub (tools/scaffold.py): need() prefers any real implementation of the same name."""
    fn.stub = True
    return fn


def norm_unit(u):
    """(pint unit, display label); a unit pint cannot parse becomes a dimensionless count with its label."""
    u = str(u or "").strip()
    p = UNIT_ALIASES.get(u, u)
    if not u or p in LOG_UNITS:
        return "", u
    try:
        ureg.Unit(p)
        return p, u
    except Exception:
        return "", u


# ---------------------------------------------------------------- files (repository or bundle)
_BUNDLE, _cache = None, {}


def use_bundle(files, deliv):
    global _BUNDLE, DELIV
    _BUNDLE, DELIV = files, Path(deliv)
    _cache.clear()


def files(pattern):
    if _BUNDLE is not None:
        return sorted(k for k in _BUNDLE if fnmatch.fnmatch(k, pattern))
    return sorted(str(p.relative_to(ROOT)) for p in ROOT.glob(pattern) if p.is_file())


def read(rel):
    if _BUNDLE is None:
        return (ROOT / rel).read_text(encoding="utf-8")
    if rel not in _BUNDLE:
        raise Unknown(f"{rel} is not part of this bundle")
    return _BUNDLE[rel]


def yload(rel):
    if rel not in _cache:
        _cache[rel] = yaml.safe_load(read(rel)) or {}
    return _cache[rel]


def read_deliv(rel):
    """Text of deliverables/<rel> (full-tier artefacts, e.g. a CAD mass report)."""
    if not (DELIV / rel).is_file():
        raise Unknown(f"deliverables/{rel} not produced yet")
    return (DELIV / rel).read_text(encoding="utf-8")


def sim_result(stem, key):
    """Quantity `key` of sim/<stem>.results.json (full-tier simulations)."""
    rel = f"sim/{stem}.results.json"
    d = json.loads(read(rel)) if rel in files("sim/*.json") else {}
    if key not in d:
        raise Unknown(f"{rel} has no '{key}' (run the simulation)")
    return Q_(d[key]["value"], norm_unit(d[key].get("unit"))[0])


def _hash(patterns):
    h = hashlib.sha256()
    for pat in patterns:
        for rel in files(pat):
            h.update(rel.encode() + b"\0" + read(rel).encode("utf-8") + b"\0")
    return h.hexdigest()[:16]


def sources_hash():
    """Everything check results depend on (staleness of build/ and deliverables/)."""
    return _hash(("ddlib.py", "params/*.yaml", "requirements/*.yaml", "checks/req_*.py", "model/*.py", "bom/*.yaml",
                  "sim/*.json"))


def docs_hash():
    """Document sources (staleness of a render)."""
    return _hash(("docs/*", "docs/parts/*", "evidence/*", "research/*", "blocked.yaml"))


# ---------------------------------------------------------------- parameters
def qty(e):
    """Value of a parameter entry: Quantity with a unit, plain number without, raw value otherwise."""
    v = e.get("value") if isinstance(e, dict) else e
    if isinstance(v, bool) or not isinstance(v, (int, float)):
        return v
    u = norm_unit(e.get("unit"))[0] if isinstance(e, dict) else ""
    return Q_(v, u) if u else v


class Params:
    """Parameters of one topology (params/common.yaml overlaid by params/<T>.yaml): P.name -> Quantity."""

    def __init__(self, T, entries, info, features):
        self.__dict__.update(topology=T, _e=entries, info=info, features=set(features))

    def __getattr__(self, k):
        if k.startswith("__"):
            raise AttributeError(k)
        reads = getattr(_trace, "params", None)
        if reads is not None:
            reads.add(k)
        if k not in self._e:
            raise Unknown(f"parameter '{k}' is not defined (topology {self.topology})")
        return qty(self._e[k])

    __getitem__ = __getattr__

    def has(self, k):
        return k in self._e

    def entry(self, k):
        return self._e[k]

    def names(self):
        return list(self._e)

    def with_(self, **over):
        """Copy with values replaced (numbers in the parameter's own unit, or Quantities)."""
        e = dict(self._e)
        for k, v in over.items():
            if k not in e:
                raise KeyError(f"cannot override unknown parameter '{k}'")
            u = norm_unit(e[k].get("unit"))[0]
            e[k] = dict(e[k], value=float(v.to(u or "").magnitude) if isinstance(v, Q_) else v)
        return Params(self.topology, e, self.info, self.features)


def topologies():
    """{id: data} of every params/<id>.yaml except common.yaml."""
    out = {}
    for rel in files("params/*.yaml"):
        if Path(rel).name != "common.yaml":
            d = yload(rel)
            out[str((d.get("topology") or {}).get("id") or Path(rel).stem)] = d
    return out


def common():
    return yload("params/common.yaml") if "params/common.yaml" in files("params/*.yaml") else {}


def meta():
    return common().get("meta") or {}


def chosen():
    """meta.chosen, else the winner of build/decision.json, else the first topology."""
    tops, t = topologies(), meta().get("chosen")
    if t not in tops and _BUNDLE is None:
        try:
            t = json.loads((BUILD / "decision.json").read_text())["winner"]
        except Exception:
            t = None
    return t if t in tops else (sorted(tops)[0] if tops else None)


def load_params(T=None):
    if not topologies() and T in (None, PLACEHOLDER):     # no topology file yet: the shared parameters alone
        return Params(PLACEHOLDER, dict(common().get("params") or {}), {"id": PLACEHOLDER, "name": "no topology yet"}, [])
    T = T or chosen()
    top = topologies().get(T)
    if top is None:
        raise SystemExit(f"unknown topology {T!r}; params/ has {sorted(topologies())}")
    entries = dict(common().get("params") or {})
    entries.update(top.get("params") or {})
    return Params(T, entries, top.get("topology") or {}, top.get("features") or [])


# ---------------------------------------------------------------- requirements
def requirements():
    """{id: requirement} from requirements/*.yaml in file order."""
    if "_reqs" not in _cache:
        _cache["_reqs"] = {str(r["id"]): dict(r, _file=rel) for rel in files("requirements/*.yaml")
                           for r in yload(rel).get("requirements") or []}
    return _cache["_reqs"]


def input_ids():
    """Requirement ids of INPUT/requirements.md, parsed exactly like the package checker."""
    if _BUNDLE is not None:
        return json.loads(_BUNDLE["_ids.json"])
    p = RUN / "INPUT" / "requirements.md"
    text = p.read_text(encoding="utf-8", errors="replace") if p.is_file() else ""
    ids = list(dict.fromkeys(m.group(1) for m in map(HEADING_ID.match, (s.strip() for s in text.splitlines())) if m))
    return ids or list(dict.fromkeys(m.group(1) for m in LOOSE_ID.finditer(text)))


def ordered_ids():
    ids = input_ids()
    return ids + [i for i in requirements() if i not in ids]


class Req:
    """Requirement handed to compute(P, R): R.cond(name), R.target, R.threshold (Quantities), R.unit."""

    def __init__(self, d):
        self.d, self.id, self.unit = d, d["id"], norm_unit(d.get("unit"))[0]

    def cond(self, name):
        c = (self.d.get("conditions") or {}).get(name)
        if c is None:
            raise Unknown(f"condition '{name}' is not defined in requirement {self.id}")
        return qty(c)

    def _q(self, k):
        v = self.d.get(k)
        return Q_(v, self.unit) if isinstance(v, (int, float)) and not isinstance(v, bool) and self.unit else v

    target = property(lambda self: self._q("target"))
    threshold = property(lambda self: self._q("threshold"))


# ---------------------------------------------------------------- model dispatch and built-ins
def _modules(T):
    if ("_mods", T) not in _cache:
        rels = [r for r in files("model/*.py") if Path(r).name == f"topo_{T}.py"]
        rels += [r for r in files("model/*.py") if not Path(r).name.startswith(("topo_", "_"))]
        _cache[("_mods", T)] = [(importlib.import_module("model." + Path(r).stem), r) for r in rels]
    return _cache[("_mods", T)]


def find(T, name):
    """(function, 'file:name') providing model quantity `name` for topology T, or (None, None). Order: topo_<T>.py,
    shared modules, built-ins; a @stub only when nothing else provides the name."""
    fallback = (None, None)
    for mod, rel in _modules(T):
        f = getattr(mod, name, None)
        if callable(f) and getattr(f, "__module__", None) == mod.__name__:   # defined there, not imported
            if not getattr(f, "stub", False):
                return f, f"{rel}:{name}"
            fallback = fallback if fallback[0] else (f, f"{rel}:{name}")
    return (BUILTINS[name], f"ddlib.py:{name}") if name in BUILTINS else fallback


def need(P, name, *args, **kw):
    """Model quantity `name` for P's topology: model/topo_<T>.py, then shared modules, then built-ins."""
    fn, where = find(P.topology, name)
    if fn is None:
        raise Unknown(f"model function '{name}' is not provided for topology {P.topology}")
    tr = getattr(_trace, "items", None)
    if tr is not None and where not in tr:
        tr.append(where)
    return fn(P, *args, **kw)


def _frac(x):
    return float(x.to("").magnitude) if isinstance(x, Q_) else float(x)


def _fx(P, frm, to):
    if frm == to:
        return 1.0
    if not P.has(f"fx_{frm}_{to}"):
        raise Unknown(f"exchange rate parameter fx_{frm}_{to} missing (value in {to}/{frm}, SOURCE with date)")
    v = getattr(P, f"fx_{frm}_{to}")
    return float(v.magnitude if isinstance(v, Q_) else v)


def rollup(P):
    """Cost roll-up of bom/<T>.yaml at 1/100/1000 units (format in README)."""
    rel = f"bom/{P.topology}.yaml"
    if rel not in files("bom/*.yaml"):
        raise Unknown(f"{rel} does not exist yet")
    b = yload(rel)
    cur = b.get("currency") or meta().get("currency") or "EUR"
    lines, bom, one_off, other = [], dict.fromkeys(QS, 0.0), 0.0, {}
    for it in b.get("items") or []:
        n = it.get("qty", 1)
        if isinstance(n, str) and n.startswith("="):
            n = _frac(getattr(P, n[1:]))
            n = int(n) if n.is_integer() else n
        price = it.get("price") or []
        if len(price) != 3 or None in price:
            raise Unknown(f"{rel} item {it.get('item')}: price must be [q1, q100, q1000]")
        fx = _fx(P, it.get("currency") or cur, cur)
        lines.append(dict(it, qty=n, ext=[float(n) * float(p) * fx for p in price]))
        for q, x in zip(QS, lines[-1]["ext"]):
            bom[q] += x
    for name, e in (b.get("other") or {}).items():
        fx = _fx(P, e.get("currency") or cur, cur)
        if "one_off" in e:
            one_off += float(e["one_off"]) * fx
            over = e.get("amortise_over", "batch")
            other[name] = {q: float(e["one_off"]) * fx / (NQ[q] if over == "batch" else float(over))
                           if q in (e.get("at") or QS) else 0.0 for q in QS}
        else:
            other[name] = {q: float(e.get(q) or 0) * fx for q in QS}
    unit = {q: bom[q] + sum(o[q] for o in other.values()) for q in QS}
    return {"currency": cur, "lines": lines, "bom_cost": bom, "other": other, "unit_cost": unit, "one_off": one_off}


def _cost_fn(kind, q):
    return lambda P: Q_(rollup(P)[kind][q], rollup(P)["currency"])


def retail_price(P):
    """Retail price incl. VAT = unit_cost_q1000 / (1 - gross_margin - channel_fees) * (1 + vat)."""
    m, f, v = (_frac(getattr(P, k)) for k in ("gross_margin", "channel_fees", "vat"))
    if m + f >= 1:
        raise Unknown("gross_margin + channel_fees must be below 1")
    return need(P, "unit_cost_q1000") / (1 - m - f) * (1 + v)


BUILTINS = {f"{k}_{q}": _cost_fn(k, q) for k in ("bom_cost", "unit_cost") for q in QS}
BUILTINS.update(one_off_cost=lambda P: Q_(rollup(P)["one_off"], rollup(P)["currency"]), retail_price=retail_price)


# ---------------------------------------------------------------- check engine
def _mag(v, unit):
    if isinstance(v, Q_):
        return float(v.to(unit or "dimensionless").magnitude)
    if isinstance(v, numbers.Real) and not isinstance(v, bool):
        if unit:            # also %, ppm: is 0.2 a fraction or 0.2 %? Only a Quantity says
            raise TypeError(f"bare number returned for a requirement in {unit}; return a Quantity, e.g. Q_(x, '{unit}')")
        return float(v)
    raise TypeError(f"compute returned {type(v).__name__}; expected a Quantity, number, bool or (bool, bool)")


def _r6(x):
    return float(f"{x:.6g}") if isinstance(x, float) and math.isfinite(x) else x


def verdict(rd, v):
    """Status from the requirement data, never from the check: PASS meets the target, PARTIAL only the
    threshold, FAIL misses the threshold. bool/argued: v is a bool or (target_met, threshold_met)."""
    sense = rd.get("sense") or "argued"
    if sense in ("bool", "argued"):
        if isinstance(v, tuple) and len(v) == 2:
            t, th = bool(v[0]), bool(v[1])
            return {"status": "PASS" if t else "PARTIAL" if th else "FAIL",
                    "value": "target" if t else "threshold" if th else "neither"}
        if type(v).__name__ in ("bool", "bool_"):
            return {"status": "PASS" if v else "FAIL", "value": "yes" if v else "no"}
        raise TypeError(f"{sense} requirement: compute must return a bool or (target_met, threshold_met)")
    x = _mag(v, norm_unit(rd.get("unit"))[0])
    t, th = rd.get("target"), rd.get("threshold")
    ref = th if th is not None else t
    if ref is None:
        raise TypeError("numeric requirement without target and threshold")
    if sense == "range":
        ok = lambda b: b is None or b[0] <= x <= b[1]
        m, width = min(x - ref[0], ref[1] - x), ref[1] - ref[0]
        mr = m / width if width else None
    else:
        sgn = 1 if sense == ">=" else -1
        ok = lambda b: b is None or sgn * (x - b) >= 0
        m = sgn * (x - ref)
        mr = m / abs(ref) if ref else None
    if t is not None and ok(t):
        status = "PASS"
    elif th is not None and ok(th):
        status = "PARTIAL" if t is not None else "PASS"
    else:
        status = "FAIL"
    return {"status": status, "value": _r6(x), "margin": _r6(m), "margin_rel": _r6(mr)}


def _blank(rid, T, status, reason=""):
    rd = requirements().get(rid, {})
    return {"id": rid, "topology": T, "status": status, "value": None, "unit": rd.get("unit_label") or rd.get("unit") or "",
            "margin": None, "margin_rel": None, "evidence": [], "params": [], "reason": reason}


def _alarm(signum, frame):
    raise TimeoutError("check took longer than its time limit")


def evaluate(rid, compute, P, rd=None, timeout=60):
    """Result of one check; `params` lists the parameters it read (decide.py re-runs only the checks a
    perturbed parameter can change)."""
    rd = rd or requirements().get(rid) or {"id": rid, "sense": "argued"}
    res, _trace.items, _trace.params = _blank(rid, P.topology, "ERROR"), [], set()
    alarm = bool(timeout) and threading.current_thread() is threading.main_thread()
    if alarm:
        signal.signal(signal.SIGALRM, _alarm)
        signal.alarm(int(timeout))
    try:
        res.update(verdict(rd, compute(P, Req(rd))))
    except Unknown as e:
        res.update(status="UNKNOWN", reason=str(e), evidence=[e.evidence] if e.evidence else [])
    except Exception as e:
        res.update(status="ERROR", reason=f"{type(e).__name__}: {e}"[:300])
    finally:
        if alarm:
            signal.alarm(0)
        res["evidence"] += _trace.items
        res["params"] = sorted(_trace.params)
        _trace.items = _trace.params = None
    return res


def _text_module(name, rel):
    mod = types.ModuleType(name)
    mod.__file__ = rel if _BUNDLE is not None else str(ROOT / rel)
    exec(compile(read(rel), rel, "exec"), mod.__dict__)
    return mod


def load_check(rid):
    if ("_chk", rid) not in _cache:
        rel = f"checks/req_{rid}.py"
        _cache[("_chk", rid)] = _text_module("check_" + re.sub(r"\W", "_", rid), rel) if rel in files("checks/req_*.py") else None
    return _cache[("_chk", rid)]


def evaluate_all(P, ids=None, timeout=60):
    """{id: result} for the given ids (None: every INPUT and derived requirement)."""
    out = {}
    for rid in ordered_ids() if ids is None else ids:
        try:
            mod = load_check(rid)
        except Exception as e:
            out[rid] = _blank(rid, P.topology, "ERROR", f"check does not load: {type(e).__name__}: {e}"[:300])
            continue
        if mod is None or not callable(getattr(mod, "compute", None)):
            out[rid] = _blank(rid, P.topology, "MISSING", f"no checks/req_{rid}.py with compute(P, R)")
        else:
            out[rid] = evaluate(rid, mod.compute, P, requirements().get(rid), timeout)
    return out


def run_check(path, compute):
    """Main of a check file: ddpy design/checks/req_<ID>.py [--topology T] prints one JSON line."""
    T = sys.argv[sys.argv.index("--topology") + 1] if "--topology" in sys.argv else chosen()
    print(json.dumps(evaluate(Path(path).stem[4:], compute, load_params(T)), default=str))


def outputs(P):
    """{name: {value, unit, where | reason}} for the OUTPUTS of the model modules and the key figures."""
    names = []
    for mod, _ in _modules(P.topology):
        names += [n for n in (getattr(mod, "OUTPUTS", None) or []) if n not in names]
    names += [n for n in KEY_FIGURES + tuple(f"unit_cost_{q}" for q in QS) + ("retail_price",) if n not in names]
    out = {}
    for n in names:
        _trace.items = []
        try:
            v = need(P, n)
            q = isinstance(v, Q_)
            out[n] = {"value": _r6(float(v.magnitude if q else v)) if q or isinstance(v, (int, float)) else v,
                      "unit": f"{v.units:~P}" if q else "", "where": (_trace.items or [""])[0]}
        except Exception as e:
            out[n] = {"value": None, "unit": "", "reason": str(e) if isinstance(e, Unknown) else f"ERROR {type(e).__name__}: {e}"}
        finally:
            _trace.items = None
    return out


def selftests():
    """Run every selftest_* of the model modules: [(name, ok, message)]."""
    res = []
    for rel in files("model/*.py"):
        if Path(rel).name.startswith("_"):
            continue
        try:
            mod = importlib.import_module("model." + Path(rel).stem)
        except Exception as e:
            res.append((rel, False, f"import: {type(e).__name__}: {e}"))
            continue
        for n in sorted(x for x in dir(mod) if x.startswith("selftest_") and callable(getattr(mod, x))):
            try:
                getattr(mod, n)()
                res.append((f"{rel}:{n}", True, ""))
            except Exception as e:
                res.append((f"{rel}:{n}", False, f"{type(e).__name__}: {e}"))
    return res


def check_source(rid):
    """Source of compute() of checks/req_<rid>.py (for the requirements document)."""
    rel = f"checks/req_{rid}.py"
    if rel not in files("checks/req_*.py"):
        return "# no check file"
    text = read(rel)
    node = next((n for n in ast.parse(text).body if isinstance(n, ast.FunctionDef) and n.name == "compute"), None)
    return ast.get_source_segment(text, node) if node else text


# ---------------------------------------------------------------- scoring, ranking
def score(results, weights=None):
    """Priority-weighted status score, weighted margin, knock-outs (FAIL at priority >= 4), counts."""
    reqs, tw, s, m, mw, counts, knock = requirements(), 0.0, 0.0, 0.0, 0.0, {}, []
    for rid, r in results.items():
        pr = (reqs.get(rid) or {}).get("priority") or 1
        w = float(weights.get(rid, pr) if weights else pr)
        counts[r["status"]] = counts.get(r["status"], 0) + 1
        tw, s = tw + w, s + w * STATUS_SCORE.get(r["status"], 0.0)
        if isinstance(r.get("margin_rel"), (int, float)):
            m, mw = m + w * max(-1.0, min(1.0, r["margin_rel"])), mw + w
        if r["status"] == "FAIL" and pr >= 4:
            knock.append(rid)
    return {"score": round(s / tw, 4) if tw else 0.0, "margin": round(m / mw, 4) if mw else 0.0,
            "knockouts": knock, "counts": counts}


def rank(rows, tol=0.02):
    """Order rows {score, margin, knockouts, unit_cost_q1000}: fewest knock-outs, then score; scores within
    `tol` of the leader tie and the lower unit cost wins, then the higher score, then the larger weighted margin.
    Returns (rows, tie_broken_by_cost)."""
    rows = sorted(rows, key=lambda r: (len(r["knockouts"]), -r["score"], -r["margin"]))
    if not rows:
        return rows, False
    lead = rows[0]
    tied = [r for r in rows if len(r["knockouts"]) == len(lead["knockouts"]) and lead["score"] - r["score"] <= tol]
    best = min(tied, key=lambda r: (math.inf if r.get("unit_cost_q1000") is None else r["unit_cost_q1000"],
                                    -r["score"], -r["margin"]))
    if best is lead:
        return rows, False
    rows.remove(best)
    return [best] + rows, True


# ---------------------------------------------------------------- exploration
def span(T, name, n=11):
    """n values across the `range` of parameter `name` of topology T."""
    lo, hi = load_params(T).entry(name)["range"]
    return [lo + (hi - lo) * i / (n - 1) for i in range(n)]


def _points(T, combos, checks):
    bad = [i for i in checks if i not in ordered_ids()]
    if bad:
        raise ValueError(f"unknown requirement id(s) {bad}")
    pts, P0 = [], load_params(T)
    base = evaluate_all(P0, timeout=0)
    swept = {k for over in combos for k in over}
    dep = [i for i, r in base.items() if swept & set(r["params"])]     # only these checks can change
    for over in combos:
        P = P0.with_(**over)
        res = dict(base, **(evaluate_all(P, dep, timeout=0) if dep else {}))
        try:
            cost = float(need(P, "unit_cost_q1000").magnitude)
        except Exception:
            cost = None
        pts.append(dict(score(res), unit_cost_q1000=cost, x=over,
                        checks={i: {"status": res[i]["status"], "value": res[i]["value"]} for i in checks}))
    return pts


def context_hash(T, exclude=()):
    """Everything a sweep of topology T depends on except the swept parameters (render and verify flag sweeps
    whose context changed afterwards)."""
    P = load_params(T)
    h = hashlib.sha256(json.dumps([{k: P.entry(k) for k in P.names() if k not in exclude}, sorted(P.features)],
                                  sort_keys=True, default=str).encode())
    h.update(_hash(("ddlib.py", "requirements/*.yaml", "checks/req_*.py", "model/*.py", f"bom/{T}.yaml")).encode())
    return h.hexdigest()[:16]


def _finish(fig, tag, data, pts):
    import matplotlib.pyplot as plt
    (BUILD / "plots").mkdir(parents=True, exist_ok=True)
    fig.tight_layout()
    fig.savefig(BUILD / "plots" / f"{tag}.png", dpi=110)
    plt.close(fig)
    best = rank(list(pts))[0][0]
    data.update(tag=tag, plot=f"plots/{tag}.png", points=pts, context_sha=context_hash(data["topology"], [v["name"] for v in data["vars"]]),
                best={"x": best["x"], "score": best["score"], "unit_cost_q1000": best["unit_cost_q1000"]})
    (BUILD / f"explore-{tag}.json").write_text(json.dumps(data, indent=1, default=str))
    return best


def _label(P, name):
    u = P.entry(name).get("unit")
    return f"{name} [{u}]" if u else name


def sweep(T, name, values=None, checks=(), tag=None, title=None):
    """1-D sweep of parameter `name` of topology T (values default to its range): weighted score and each listed
    check against its target (green) and threshold (red). Writes build/explore-<tag>.json and
    build/plots/<tag>.png; returns the best point."""
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    values, P0, reqs = list(values) if values is not None else span(T, name), load_params(T), requirements()
    pts = _points(T, [{name: v} for v in values], checks)
    fig, axes = plt.subplots(1 + len(checks), 1, figsize=(7, 2.3 * (1 + len(checks))), sharex=True, squeeze=False)
    axes[0][0].plot(values, [p["score"] for p in pts], "k.-")
    axes[0][0].set_ylabel("weighted score")
    for (ax,), rid in zip(axes[1:], checks):
        ax.plot(values, [p["checks"][rid]["value"] if isinstance(p["checks"][rid]["value"], (int, float)) else math.nan
                         for p in pts], "b.-")
        for k, col in (("target", "g"), ("threshold", "r")):
            b = reqs[rid].get(k)
            for y in (b if isinstance(b, list) else [b]):
                if isinstance(y, (int, float)) and not isinstance(y, bool):
                    ax.axhline(y, color=col, ls="--", lw=1)
        ax.set_ylabel(f"{rid}\n[{reqs[rid].get('unit_label') or reqs[rid].get('unit', '')}]", fontsize=8)
    axes[-1][0].set_xlabel(_label(P0, name))
    fig.suptitle(title or f"Topology {T}: sweep of {name}", fontsize=10)
    return _finish(fig, tag or f"{T}-{name}", {"kind": "sweep", "topology": T, "title": title or f"Sweep of {name} (topology {T})",
                   "vars": [{"name": name, "unit": P0.entry(name).get("unit", ""), "values": values}],
                   "current": {name: P0.entry(name)["value"]}, "checks": list(checks)}, pts)


def grid(T, x, xs, y, ys, checks=(), tag=None, title=None):
    """2-D design-space map of parameters x, y of topology T: weighted score as colour, the threshold contour of
    each listed check, the current design (star) and the best grid point (circle). Returns the best point."""
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import numpy as np
    P0, reqs = load_params(T), requirements()
    pts = _points(T, [{x: xv, y: yv} for yv in ys for xv in xs], checks)
    S = np.array([p["score"] for p in pts]).reshape(len(ys), len(xs))
    fig, ax = plt.subplots(figsize=(7, 5.2))
    fig.colorbar(ax.pcolormesh(xs, ys, S, shading="nearest", cmap="viridis"), ax=ax, label="weighted score")
    for k, rid in enumerate(checks):
        V = np.array([p["checks"][rid]["value"] if isinstance(p["checks"][rid]["value"], (int, float)) else np.nan
                      for p in pts], dtype=float).reshape(len(ys), len(xs))
        th = reqs[rid].get("threshold", reqs[rid].get("target"))
        if isinstance(th, (int, float)) and not isinstance(th, bool) and np.isfinite(V).sum() > 3:
            ax.clabel(ax.contour(xs, ys, V, levels=[th], colors=[f"C{k + 1}"], linewidths=1.5), fmt={th: rid}, fontsize=7)
    best = rank(list(pts))[0][0]
    ax.plot([P0.entry(x)["value"]], [P0.entry(y)["value"]], "w*", ms=14, mec="k", label="current design")
    ax.plot([best["x"][x]], [best["x"][y]], "o", mfc="none", mec="r", ms=12, label="best grid point")
    ax.set_xlabel(_label(P0, x))
    ax.set_ylabel(_label(P0, y))
    ax.legend(loc="best", fontsize=7)
    ax.set_title(title or f"Topology {T}: design space {x} x {y}", fontsize=10)
    return _finish(fig, tag or f"{T}-{x}-{y}", {"kind": "grid", "topology": T, "title": title or f"Design space {x} x {y} (topology {T})",
                   "vars": [{"name": n, "unit": P0.entry(n).get("unit", ""), "values": list(v)} for n, v in ((x, xs), (y, ys))],
                   "current": {x: P0.entry(x)["value"], y: P0.entry(y)["value"]}, "checks": list(checks)}, pts)


# ---------------------------------------------------------------- formatting
def fmt(x, unit="", sig=3):
    """3 significant digits, no exponent between 1e-3 and 1e6; None -> 'n/a'."""
    if x is None:
        return "n/a"
    if isinstance(x, Q_):                               # pint writes litre "l": print "L", "mL"
        x, unit = float(x.magnitude), re.sub(r"(?<![A-Za-z])([mµcd]?)l(?![A-Za-z])", r"\1L", f"{x.units:~P}")
    if isinstance(x, bool) or not isinstance(x, (int, float)):
        return f"{x} {unit}".strip()
    x = 0 if x == 0 else x                              # no "-0"
    if isinstance(x, int) or x == 0 or not math.isfinite(x):
        s = f"{x:g}"
    elif abs(x) >= 1e6 or abs(x) < 1e-3:
        s = f"{x:.{sig}g}"
    else:
        s = f"{x:.{max(0, sig - 1 - math.floor(math.log10(abs(x))))}f}"
    return f"{s} {unit}".strip()


def fmt_param(e):
    """A parameter or condition entry {value, unit} as text with its unit label, e.g. '350 Δ°C'."""
    e = e if isinstance(e, dict) else {"value": e}
    q, v = qty(e), e.get("value")
    if isinstance(q, Q_):
        return fmt(q)
    return fmt(v, norm_unit(e.get("unit"))[1]) if isinstance(v, (int, float)) and not isinstance(v, bool) else str(v)
files/method/repo-template/docs/benchmark.md.j2 (12 wierszy)
# Benchmark — comparable products

Data: `design/research/benchmark.yaml`; every line was read on the page given, on the date given (SOURCE).

| product | maker | price | key specifications | weak points (reviews, teardowns, forums) | source, date read |
|---|---|---|---|---|---|
{% for b in benchmark %}
| {{ b.get("name", "") }} | {{ b.get("maker", "") }} | {{ b.get("price", "") }} | {{ kv(b.get("specs")) }} | {{ (b.get("weak_points") or []) | join("; ") }} | {{ b.get("url", "") }} ({{ b.get("read", "") }}) |
{% endfor %}

{{ part("benchmark") }}
files/method/repo-template/docs/checks.md.j2 (32 wierszy)
# Requirement check results — all topologies

Generated {{ generated }} by `design/checks/run_all.py`: every check `design/checks/req_<id>.py` evaluated on every topology. Status from the check engine: PASS meets the target, PARTIAL meets only the threshold, FAIL misses the threshold, NOT RUN not computable yet (reason below; nothing is claimed), BLOCKED the check itself fails. Results are analysis, not tests; recompute with `calc/model_<topology>.py`.

| topology | name | result |
|---|---|---|
{% for t in topos %}
| {{ t.id }} | {{ t.name }} | {{ scoreline(t.id) }} |
{% endfor %}

| requirement | priority | acceptance | {{ topos | map(attribute="id") | join(" | ") }} |
|---|---|---|{{ "---|" * (topos | length) }}
{% for r in reqs %}
| {{ r.id }} {{ r.get("title", "") }} | {{ r.get("priority", "") }} | {{ bounds(r) }} | {{ cells(r.id) }} |
{% endfor %}

## Reasons for NOT RUN and BLOCKED
{% for r in reqs %}
{% for t in topos %}
{% if c(r.id, t.id).status in ("NOT RUN", "BLOCKED") %}
- {{ r.id }} [{{ t.id }}] {{ c(r.id, t.id).status }}: {{ c(r.id, t.id).get("reason", "") }}
{% endif %}
{% endfor %}
{% endfor %}

## Model selftests (known-answer tests)
{% for s in selftests %}
- {{ "ok" if s.ok else "FAILED" }} `{{ s.name }}` {{ s.message }}
{% else %}
- none
{% endfor %}
files/method/repo-template/docs/concept.md.j2 (123 wierszy)
# Concept — {{ product }}

Generated from the design repository (`design/`) with the check results of {{ generated }}. Method: every requirement became an executable check before design; each alternative is a parameter set (`params/<topology>.yaml`, copied to `params/`) plus model modules (`model.md`); the checks were run on every alternative and its design variables were swept against them; the choice follows a fixed rule. Every number here is quoted from those sources (origin labels as recorded); `calc/model_<topology>.py` recomputes all check results offline.

## 1. The product
{{ part("product") }}

## 2. Functions and flows
{{ part("functions") }}

## 3. Alternatives
The deciding physics and how each discipline is modelled: `physics-map.md`. Shared inputs of all alternatives are listed in section 6.
{% for t in topos %}

### 3.{{ loop.index }} Topology {{ t.id }} — {{ t.name }}
{{ t.principle }}

{{ part("topo_" ~ t.id) }}

| parameter | value | kind | origin | note |
|---|---|---|---|---|
{% for e in t.entries %}
| {{ e.name }} | {{ e.text }} | {{ e.kind }} | {{ e.origin }} | {{ e.note }} |
{% endfor %}

Model outputs: {{ outline(t.id) or "none computed" }}.

Checks: {{ scoreline(t.id) }}.
{% endfor %}

## 4. Evaluation
Per-requirement results of every alternative: `check-results.md`.

### 4.1 Design-space exploration
{% for x in explore %}

**{{ x.title }}** — {{ x.kind }} over {{ x.vars | map(attribute="name") | join(" × ") }}; checks drawn: {{ x.checks | join(", ") or "weighted score only" }}. Best point: {{ pt(x, "best") }} (weighted score {{ "%.3f" | format(x.best.score) }}). Design values now: {{ now(x) }}; the plot marks the design at sweep time ({{ pt(x, "current") }}).{{ " **This exploration predates later changes to the model or to other parameters of topology " ~ x.topology ~ ": indicative only.**" if x.stale else "" }}

![{{ x.tag }}]({{ x.plot }})
{% else %}
[[WRITE: no exploration yet - run the explore scripts]]
{% endfor %}

### 4.2 Decision rule, ranking and robustness
{% if decision %}
Rule (`method/POLICY.md`, applied by `design/tools/decide.py`): fewest knock-outs (FAIL of a requirement with priority ≥ {{ decision.rule.knockout_priority }}), then the priority-weighted status score (PASS {{ decision.rule.status_score.PASS }}, PARTIAL {{ decision.rule.status_score.PARTIAL }}, other {{ decision.rule.status_score.other }}); scores within {{ decision.rule.tie_tolerance }} of the leader are a tie, broken by the lower unit cost at 1000 units.

| rank | topology | weighted score | weighted margin | knock-outs | unit cost at 1000 |
|---|---|---|---|---|---|
{% for r in decision.ranking %}
| {{ loop.index }} | {{ r.topology }} — {{ r.name }} | {{ "%.3f" | format(r.score) }} | {{ "%+.3f" | format(r.margin) }} | {{ r.knockouts | join(", ") or "none" }} | {{ fmt(r.unit_cost_q1000, currency) }} |
{% endfor %}

Winner: **{{ decision.winner }}**{{ " (tie broken by cost)" if decision.tie_broken_by_cost else "" }}. Robustness: each requirement weight multiplied by {{ decision.rule.weight_steps | join(" and ") }} changes the winner in {{ decision.weight_flips | length }} case(s){{ (": " ~ (decision.weight_flips | map(attribute="requirement") | join(", "))) if decision.weight_flips else "" }}; with all weights equal the winner is {{ decision.equal_weights_winner }}. {{ decision.assumption_effects | length }} ASSUMPTION range end(s) were tested on every topology that uses them (effects: assumptions register in `01-requirements/requirements.md`){{ "; " ~ (decision.assumptions_untested | length) ~ " not tested (time budget)" if decision.assumptions_untested else "" }}. Model coverage: {{ (decision.coverage.uneven | length) ~ " requirement(s) computed for some topologies only (" ~ (decision.coverage.uneven | join(", ")) ~ ")" if decision.coverage.uneven else "every computed requirement is computed for every topology" }}{{ "; NOT RUN for every topology: " ~ (decision.coverage.not_run_everywhere | join(", ")) if decision.coverage.not_run_everywhere else "" }}. Overall: {{ "robust" if decision.robust else "**NOT robust**: " ~ (decision.not_robust_because | join("; ")) }}.
{% else %}
[[WRITE: run make decide]]
{% endif %}

{{ part("decision") }}

## 5. Chosen concept: {{ T }} — {{ chosen.name }}
{{ part("architecture") }}

### Rough bill of materials (`bom/{{ T }}.yaml`)
{% if cost[T] %}
| item | qty | description | manufacturer, part number | unit price at 1 / 100 / 1000 | origin | notes |
|---|---|---|---|---|---|---|
{% for ln in cost[T].lines %}
| {{ ln.get("item", "") }} | {{ fmt(ln.qty) }} | {{ ln.get("description", "") }} | {{ ln.get("manufacturer", "") }} {{ ln.get("mpn", "") }} | {{ ln.price | join(" / ") }} {{ ln.get("currency") or cost[T].currency }} | {{ ln.get("origin", "ASSUMPTION") }} | {{ ln.get("notes", "") }} |
{% endfor %}
{% else %}
[[WRITE: bom/{{ T }}.yaml missing]]
{% endif %}

## 6. Key estimates of the chosen concept
| quantity | value | origin |
|---|---|---|
{% for n, x in outputs[T].items() %}
| {{ n }} | {{ fmt(x.value, x.unit) if x.value is not none else "not computed: " ~ x.get("reason", "") }} | {{ ("CALCULATION `" ~ x.where ~ "`") if x.get("where") else "—" }} |
{% endfor %}

Shared inputs (`params/common.yaml`):

| parameter | value | origin | note |
|---|---|---|---|
{% for e in common %}
| {{ e.name }} | {{ e.text }} | {{ e.origin }} | {{ e.note }} |
{% endfor %}

## 7. Safety concept
{{ part("safety") }}

## 8. Compliance route
{{ part("compliance") }}

## 9. Cost
{% if cost[T] %}
| {{ cost[T].currency }} | 1 unit | 100 units | 1000 units |
|---|---|---|---|
| bill of materials | {{ money(cost[T].bom_cost) }} |
{% for k, v in cost[T].other.items() %}
| {{ k }} | {{ money(v) }} |
{% endfor %}
| **unit cost** | {{ money(cost[T].unit_cost) }} |

Retail price incl. VAT: {{ o("retail_price") }} = unit cost at 1000 units / (1 − gross margin − channel fees) × (1 + VAT), with gross margin {{ p("gross_margin") }}, channel fees {{ p("channel_fees") }} and VAT {{ p("vat") }}.{{ " Roll-up details: `09-bom-cost/cost.md`." if tier == "full" else " Rough BOM data of every topology: `bom/`." }}
{% else %}
[[WRITE: bom/{{ T }}.yaml missing - no cost]]
{% endif %}

## 10. Risks
{{ part("risks") }}

## 11. Decisions taken without a human
{{ part("decisions") }}

## 12. Open issues and first steps of detailed design
{% for rid in ids if c(rid).status in ("NOT RUN", "BLOCKED") %}
- {{ c(rid).status }} {{ rid }}: {{ c(rid).get("reason", "") }}
{% endfor %}

{{ part("open") }}
files/method/repo-template/docs/cost.md.j2 (32 wierszy)
# Cost roll-up — topology {{ T }} ({{ chosen.name }})

Generated from `design/bom/{{ T }}.yaml` (copied to `source/`) by `ddlib.py:rollup`: line cost = quantity × unit price at the order quantity; other costs per unit (labour, packaging, freight, amortised one-off costs) as recorded with their origin. Prices read on a supplier page carry the date in their notes; all others are ASSUMPTION.
{% if cost[T] %}

## Parts ({{ cost[T].currency }})
| item | qty | description | manufacturer | MPN | supplier | unit price 1 / 100 / 1000 | origin | notes |
|---|---|---|---|---|---|---|---|---|
{% for ln in cost[T].lines %}
| {{ ln.get("item", "") }} | {{ fmt(ln.qty) }} | {{ ln.get("description", "") }} | {{ ln.get("manufacturer", "") }} | {{ ln.get("mpn", "") }} | {{ ln.get("supplier", "") }} | {{ ln.price | join(" / ") }} {{ ln.get("currency") or cost[T].currency }} | {{ ln.get("origin", "ASSUMPTION") }} | {{ ln.get("notes", "") }} |
{% endfor %}

## Unit cost ({{ cost[T].currency }} per unit)
| | 1 unit | 100 units | 1000 units |
|---|---|---|---|
| bill of materials | {{ money(cost[T].bom_cost) }} |
{% for k, v in cost[T].other.items() %}
| {{ k }} | {{ money(v) }} |
{% endfor %}
| **unit cost** | {{ money(cost[T].unit_cost) }} |

One-off costs in total (tooling, certification, …): {{ fmt(cost[T].one_off, cost[T].currency) }}.

## Retail price
Retail price incl. VAT = unit cost at 1000 units / (1 − gross margin − channel fees) × (1 + VAT) = {{ o("retail_price") }}, with gross margin {{ p("gross_margin") }}, channel fees {{ p("channel_fees") }} and VAT {{ p("vat") }} from `params/`.

{{ part("pricing", required=False) }}
{% else %}

[[WRITE: bom/{{ T }}.yaml missing - no cost roll-up]]
{% endif %}
files/method/repo-template/docs/map.yaml (23 wierszy)
# What tools/render.py writes (maintainer only). Paths are relative to deliverables/.
# Full tier: add an entry per new document (template in docs/, out, desc, tiers). REPORT stays last.
# words: true = JSON copied with the RUN CONTRACT status words (NOT RUN, BLOCKED) instead of the engine's.
render:
  - {template: req.md.j2, out: 01-requirements/requirements.md, desc: "Given and derived requirements, each with its executable check and results"}
  - {template: checks.md.j2, out: 02-concept/check-results.md, desc: "Requirement check results of every topology"}
  - {template: concept.md.j2, out: 02-concept/concept.md, desc: "Alternatives, exploration, decision and the chosen architecture"}
  - {template: physics-map.md.j2, out: 02-concept/physics-map.md, desc: "Physics map: the deciding disciplines and their model modules"}
  - {template: benchmark.md.j2, out: 02-concept/benchmark.md, desc: "Comparable products"}
  - {template: model.md.j2, out: 02-concept/model.md, desc: "Model sources (physics and cost)"}
  - {template: cost.md.j2, out: 09-bom-cost/cost.md, tiers: [full], desc: "Cost roll-up at 1, 100 and 1000 units and the retail price"}
  - {template: top.md.j2, out: REPORT.md, desc: "Design report"}
copy:
  - {src: "requirements/*.yaml", dest: 01-requirements/source, desc: "Requirement data (source of requirements.md)"}
  - {src: "params/*.yaml", dest: 02-concept/params, desc: "Parameter files: the single home of every design number"}
  - {src: "build/check-results.json", dest: 02-concept, words: true, desc: "Check results (machine-readable)"}
  - {src: "build/decision.json", dest: 02-concept, words: true, desc: "Topology ranking and robustness tests"}
  - {src: "build/explore-*.json", dest: 02-concept/explore, words: true, desc: "Exploration data"}
  - {src: "build/plots/*.png", dest: 02-concept/plots, desc: "Sweep and design-space plots"}
  - {src: "bom/*.yaml", dest: 02-concept/bom, tiers: [concept], desc: "Rough BOM and cost data per topology"}
  - {src: "bom/*.yaml", dest: 09-bom-cost/source, tiers: [full], desc: "BOM and cost data per topology"}
  - {src: "evidence/*.md", dest: 02-concept/evidence, render: true, desc: "Argued evidence: requirements decided by documented design decisions or not computable yet"}
files/method/repo-template/docs/model.md.j2 (12 wierszy)
# Model sources — {{ product }}

The physics and cost models of every topology, verbatim from `design/model/` (each function's docstring carries its hand estimate; `selftest_*` functions are known-answer tests). The requirement checks evaluate these functions with the parameters of `params/`; built-in cost functions (`unit_cost_q*`, `retail_price`) are in `ddlib.py`, embedded in `calc/model_<topology>.py`.
{% for rel, text in modsrc() %}

## `{{ rel }}`

```python
{{ text }}
```
{% endfor %}
files/method/repo-template/docs/parts/decision.md.j2 (14 wierszy)
{# Skeleton (maintainer): keep the generated lines, replace each [[WRITE ...]]; quote numbers with p() o() c() req(). #}
{% if decision %}
By the rule, **{{ decision.winner }}** wins ({{ scoreline(decision.winner) }}).
{% for r in decision.ranking if r.topology != decision.winner %}
- {{ r.topology }} — {{ r.name }} ranks below it: {{ versus(r.topology) }}.
{% endfor %}
{% if not decision.robust %}

Not robust: {{ decision.not_robust_because | join("; ") }}. Retiring this is the first open issue.
{% endif %}
{% endif %}

[[WRITE: 3-6 sentences in engineering terms: why the winner's working principle wins, why each loser loses, and what would have to be true for a loser to be the better product]]
files/method/repo-template/docs/parts/open.md.j2 (10 wierszy)
{# Skeleton (maintainer): replace the [[WRITE ...]] marker; NOT RUN ids are listed by the documents themselves. #}
{% if decision and not decision.robust %}
- Retire what makes the concept choice fragile: {{ decision.not_robust_because | join("; ") }}.
{% endif %}
{% for x in assumptions if "winner becomes" in x.effects %}
- Replace ASSUMPTION {{ x.name }} ({{ x.scope }}) by a SOURCE or a test: {{ x.effects }}.
{% endfor %}

[[WRITE: the first steps of detailed design in order, each tied to a requirement id, a NOT RUN check or an ASSUMPTION]]
files/method/repo-template/docs/parts/risks.md.j2 (11 wierszy)
{# Skeleton (maintainer): keep the generated table, replace the [[WRITE ...]] marker. #}
Smallest computed margins of topology {{ T }}:

| requirement | status | value | margin to the threshold |
|---|---|---|---|
{% for r in tight(5) %}
| {{ r.id }} | {{ r.status }} | {{ val(r.id) }} | {{ fmt(r.margin, r.unit) }} |
{% endfor %}

[[WRITE: the top 5 risks (technical, safety, cost, schedule), each with likelihood, impact, mitigation and the requirement id or ASSUMPTION it rests on]]
files/method/repo-template/docs/physics-map.md.j2 (6 wierszy)
# Physics map — {{ product }}

Which disciplines decide this product, the governing relations, the dominant parameters and how each is modelled in `design/model/` (sources: `model.md`).

{{ part("physics") }}
files/method/repo-template/docs/req.md.j2 (48 wierszy)
# Requirements — {{ product }}

Generated by `design/tools/render.py` from `design/requirements/*.yaml` (copied to `source/`) with the check results of {{ generated }}. Every requirement has one executable check, `design/checks/req_<id>.py`, written before the design existed; its `compute(P, R)` is shown below with its result for every concept topology. The status is assigned by the check engine from the target and threshold, never by the check: PASS meets the target, PARTIAL meets only the threshold, FAIL misses the threshold, NOT RUN cannot be computed yet (reason in `02-concept/check-results.md`; nothing is claimed), BLOCKED the check itself fails. All results are analysis (CALCULATION), not tests. Standalone recomputation: `02-concept/calc/model_<topology>.py`{{ " and `05-calculations/`" if tier == "full" else "" }}.

## 1. Given requirements
{% for r in reqs if r.id in ids %}

### {{ r.id }} — {{ r.get("title", "") }}
- Statement: {{ r.get("statement", "") }}
- Metric: {{ r.get("metric") or "—" }} · target: {{ r.get("raw_target") or r.get("target") }} · threshold: {{ r.get("raw_threshold") or r.get("threshold") }} · priority: {{ r.get("priority", "") }} · verified by: {{ r.get("checked_by") or "—" }}
- Interpretation (verification condition): {{ r.get("interpretation") or "[[WRITE: interpretation of " ~ r.id ~ "]]" }}
- Design consequence: {{ r.get("consequence") or "[[WRITE: consequence of " ~ r.id ~ "]]" }}
- Check conditions: {{ conds(r) or "none" }} · acceptance: {{ bounds(r) }}
- Results: {{ cells(r.id, label=True) }}

```python
{{ src(r.id) }}
```
{% endfor %}

## 2. Derived requirements
{% set derived = reqs | rejectattr("id", "in", ids) | list %}
{% if derived %}

| id | parents | statement | metric | acceptance | verification | rationale | {{ topos | map(attribute="id") | join(" | ") }} |
|---|---|---|---|---|---|---|{{ "---|" * (topos | length) }}
{% for r in derived %}
| {{ r.id }} | {{ (r.get("parents") or []) | join(", ") }} | {{ r.get("statement", "") }} | {{ r.get("metric", "") }} | {{ bounds(r) }} | {{ r.get("verification") or r.get("checked_by") or "" }} | {{ r.get("rationale", "") }} | {{ cells(r.id) }} |
{% endfor %}
{% else %}
[[WRITE: derived requirements (requirements/*.yaml entries with kind: derived)]]
{% endif %}
{% for area, why in not_applicable %}
- Area "{{ area }}": no derived requirement — {{ why }}
{% endfor %}

## 3. Conflicts and decisions
{{ part("conflicts") }}

## 4. Assumptions register
Every ASSUMPTION parameter of `design/params/`, its reason, the range used for sensitivity, and the status changes (and any change of the chosen topology) found by moving it to each end of its range on every topology that uses it (`02-concept/decision.json`).

| parameter | scope | value | reason | range | effect at the range ends |
|---|---|---|---|---|---|
{% for x in assumptions %}
| {{ x.name }} | {{ x.scope }} | {{ x.text }} | {{ x.note }} | {{ (x.range | join(" … ")) if x.range else "—" }} | {{ x.effects }} |
{% endfor %}
files/method/repo-template/docs/top.md.j2 (63 wierszy)
# Design report — {{ product }}

Tier: {{ tier }}. Chosen concept: **{{ T }} — {{ chosen.name }}**. Rendered by the maintainer from the design repository (`design/`) with the check results of {{ generated }}. Design numbers are quoted from `design/params/` (origin label as recorded there), from the model (CALCULATION, file:function given) or from the requirement checks; only external facts (standards, competitor data) are typed, each with SOURCE and its reference. Status words of checks: PASS, PARTIAL, FAIL, NOT RUN (no model or decision computes it yet: nothing is claimed), BLOCKED (the check itself fails) — all analysis, no physical test.

## 1. The product
{{ part("product") }}

## 2. Key parameters
| parameter | value | origin |
|---|---|---|
{% for n, x in outputs[T].items() if x.value is not none %}
| {{ n }} | {{ fmt(x.value, x.unit) }} | CALCULATION `{{ x.get("where", "") }}` |
{% endfor %}
{% for e in chosen.entries if e.kind == "design" %}
| {{ e.name }} (design variable) | {{ e.text }} | {{ e.origin }}: {{ e.note }} |
{% endfor %}

## 3. Cost and price
{% if cost[T] %}
| {{ cost[T].currency }} | 1 unit | 100 units | 1000 units |
|---|---|---|---|
| bill of materials | {{ money(cost[T].bom_cost) }} |
| **unit cost** | {{ money(cost[T].unit_cost) }} |

Retail price incl. VAT: {{ o("retail_price") }}, from the unit cost at 1000 units (logic and inputs: {{ "`09-bom-cost/cost.md`" if tier == "full" else "`02-concept/concept.md` section 9" }}).
{% else %}
[[WRITE: bom/{{ T }}.yaml missing - no cost]]
{% endif %}

## 4. Status per requirement
Compliance of topology {{ T }} (`01-requirements/compliance.csv`; met = PASS, partly = PARTIAL, not_met = FAIL, not_addressed = NOT RUN or BLOCKED: nothing is claimed without a computed check):

| id | requirement | check | value | compliance |
|---|---|---|---|---|
{% for rid in ids %}
| {{ rid }} | {{ R[rid].get("title", "") if rid in R else "" }} | {{ c(rid).status }} | {{ val(rid) }} | {{ word(rid) }} |
{% endfor %}

## 5. Top risks and open hazards
{{ part("risks") }}

## 6. Decisions taken without a human
{{ part("decisions") }}
{% if decision %}

Concept choice by rule: {{ decision.ranking | map(attribute="topology") | join(" > ") }} (weighted scores {% for r in decision.ranking %}{{ "%.3f" | format(r.score) }}{{ ", " if not loop.last else "" }}{% endfor %}); {{ "robust to the weight, assumption and model-coverage tests" if decision.robust else "NOT robust: " ~ (decision.not_robust_because | join("; ")) }}.
{% endif %}

## 7. Open, blocked, not run
{% for b in blocked %}
- BLOCKED: {{ b.what }} — {{ b.why }}
{% endfor %}
{% for rid in ids if c(rid).status in ("FAIL", "NOT RUN", "BLOCKED") %}
- {{ c(rid).status }} {{ rid }}: {{ c(rid).get("reason") or ("misses the threshold by " ~ fmt(-c(rid).margin, c(rid).unit) if c(rid).margin is not none else "") }}
{% endfor %}

{{ part("open") }}

## 8. Files
{% for g, n, ex in filemap() %}
- `{{ g }}/` — {{ n }} file(s), e.g. {{ ex | join(", ") }}
{% endfor %}
files/method/repo-template/model/__init__.py (3 wierszy)
"""Physics and cost model (design/README.md "Model"): model/topo_<T>.py per topology, shared modules under any
other name. ddlib.need(P, name) finds a function in topo_<T>.py first, then in the shared modules."""
files/method/repo-template/params/common.yaml (12 wierszy)
# Parameters shared by every topology. Single home of each number: value, unit, origin, note (+ kind, range).
# origin: SOURCE (note: reference + date read) | CALCULATION (note: script) | SIMULATION | MEASUREMENT | ASSUMPTION (note: reason; give a range)
meta:
  product: ""        # short product name for document titles
  currency: EUR      # currency of every cost figure: use the one INPUT/requirements.md uses
  chosen: null       # topology id, set by the maintainer after `make decide`
params:
  # Business inputs of the retail price (ddlib.retail_price); defaults from method/POLICY.md, revise with evidence.
  gross_margin: {value: 0.40, unit: "", kind: fact, origin: ASSUMPTION, note: "POLICY default margin on the net price for direct and crowdfunding sales", range: [0.30, 0.50]}
  channel_fees: {value: 0.10, unit: "", kind: fact, origin: ASSUMPTION, note: "POLICY default: crowdfunding platform plus payment processing share of the net price", range: [0.08, 0.15]}
  vat: {value: 0.20, unit: "", kind: fact, origin: ASSUMPTION, note: "POLICY default VAT on the retail price; set the rate of the target market", range: [0.17, 0.27]}
files/method/repo-template/tools/decide.py (142 wierszy)
#!/usr/bin/env python3
"""Rank the topologies from build/check-results.json by the decision rule of method/POLICY.md and test the
ranking's robustness (make decide). Writes build/decision.json; changes nothing else.

Rule: fewest knock-outs (FAIL of a priority >= 4 requirement), then the weighted status score (weights =
priority; PASS 1, PARTIAL 0.5, else 0); scores within TIE of the leader are a tie broken by unit cost at 1000.
Robustness tests, each re-ranks all topologies:
  weights      every requirement weight x0.8 and x1.2, and all weights equal;
  assumptions  every ASSUMPTION fact (kind != design) with a `range` at both ends (a common one on every topology
               that uses it, a topology's own on that topology); only the checks that read it are re-run (design
               variables are chosen, their ranges are exploration ranges, not uncertainty);
  coverage     ids computed for some topologies but NOT RUN for others (a modelling gap, not a design result):
               the winner's gaps set to FAIL, the others' to PASS.
robust = no test changes the winner and every assumption was tested within --budget seconds.
"""
import argparse
import datetime
import json
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

TIE, STEPS = 0.02, (0.8, 1.2)


def rows_for(results_by_t, costs, names, weights=None):
    return [dict(dd.score(res, weights), topology=t, name=names.get(t, t), unit_cost_q1000=costs.get(t))
            for t, res in results_by_t.items()]


def cost_of(P):
    try:
        return float(dd.need(P, "unit_cost_q1000").magnitude)
    except Exception:
        return None


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--budget", type=float, default=300, help="seconds for the assumption tests (default 300)")
    a = ap.parse_args()
    path = dd.BUILD / "check-results.json"
    if not path.is_file():
        sys.exit("build/check-results.json missing: run make check first")
    data = json.loads(path.read_text())
    if data.get("inputs_sha") != dd.sources_hash():
        print("WARNING: check results are stale (sources changed since make check)")
    tops = data["topologies"]
    res = {t: d["results"] for t, d in tops.items()}
    names = {t: d.get("name", t) for t, d in tops.items()}
    costs = {t: (d.get("outputs", {}).get("unit_cost_q1000") or {}).get("value") for t, d in tops.items()}

    def winner_of(results, c=costs, w=None):
        return dd.rank(rows_for(results, c, names, w), TIE)[0][0]["topology"]

    ranking, tie = dd.rank(rows_for(res, costs, names), TIE)
    winner = ranking[0]["topology"]
    reqs = dd.requirements()
    base_w = {rid: float((reqs.get(rid) or {}).get("priority") or 1) for rid in dd.ordered_ids()}
    why = []                                   # every reason the result is not robust, for the decision text

    flips = []
    for rid in base_w:
        for f in STEPS:
            new = winner_of(res, w=dict(base_w, **{rid: base_w[rid] * f}))
            if new != winner:
                flips.append({"requirement": rid, "factor": f, "winner": new})
                why.append(f"weight of {rid} x{f} makes {new} the winner")
    eq = winner_of(res, w={rid: 1.0 for rid in base_w})
    if eq != winner:
        why.append(f"equal weights make {eq} the winner")

    computed = {t: {i for i, r in res[t].items() if r["status"] in dd.COMPUTED} for t in res}
    uneven = [i for i in dd.ordered_ids() if any(i in computed[t] for t in res) and not all(i in computed[t] for t in res)]
    gaps = {t: [i for i in uneven if i not in computed[t]] for t in res}
    worst = winner
    if uneven:
        alt = {t: {i: dict(r, status="FAIL" if t == winner else "PASS", margin_rel=None) if i in gaps[t] else r
                   for i, r in res[t].items()} for t in res}
        worst = winner_of(alt)
        if worst != winner:
            why.append(f"model coverage ({'; '.join(f'{t} not run: ' + ', '.join(g) for t, g in gaps.items() if g)}): "
                       f"if {winner} failed its gaps and the others passed theirs, {worst} would win")
    nowhere = [i for i in dd.ordered_ids() if i in res[winner] and not any(i in computed[t] for t in res)]

    effects, untested, t0 = [], [], time.monotonic()
    live = [t for t in res if t in dd.topologies()]            # stale results may name a removed topology
    own = {t: dd.topologies()[t].get("params") or {} for t in live}
    jobs = [(n, "common", e, [t for t in live if n not in own[t]]) for n, e in (dd.common().get("params") or {}).items()]
    jobs += [(n, t, e, [t]) for t in live for n, e in own[t].items()]
    for name, scope, e, users in jobs:
        if not users or not isinstance(e, dict) or e.get("origin") != "ASSUMPTION" or e.get("kind") == "design" \
                or not (isinstance(e.get("range"), list) and len(e["range"]) == 2):
            continue
        for end, v in zip(("lo", "hi"), e["range"]):
            if time.monotonic() - t0 > a.budget:
                untested.append(f"{name} ({scope}, {end})")
                continue
            alt, alt_cost, changes, n = dict(res), dict(costs), [], 0
            for t in users:
                P = dd.load_params(t).with_(**{name: v})
                ids = [i for i, r in res[t].items() if name in r.get("params", [name])]
                new = dd.evaluate_all(P, ids, timeout=0)
                n += len(ids)
                alt[t], alt_cost[t] = dict(res[t], **new), cost_of(P)
                changes += [{"topology": t, "id": i, "from": res[t][i]["status"], "to": r["status"]}
                            for i, r in new.items() if r["status"] != res[t][i]["status"]]
            w = winner_of(alt, alt_cost)
            effects.append({"param": name, "scope": scope, "end": end, "value": v, "unit": e.get("unit", ""),
                            "checks_rerun": n, "changes": changes, "winner": w})
            if w != winner:
                why.append(f"ASSUMPTION {name} ({scope}) at its {end} end ({dd.fmt_param(dict(e, value=v))}) "
                           f"makes {w} the winner")
    if untested:
        why.append(f"{len(untested)} assumption range end(s) not tested within the time budget")

    out = {"generated": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
           "check_results": data.get("generated"), "inputs_sha": data.get("inputs_sha"),
           "rule": {"knockout_priority": 4, "status_score": {"PASS": 1, "PARTIAL": 0.5, "other": 0},
                    "tie_tolerance": TIE, "tie_break": "unit_cost_q1000", "weight_steps": list(STEPS)},
           "ranking": ranking, "winner": winner, "tie_broken_by_cost": tie, "weight_flips": flips,
           "equal_weights_winner": eq,
           "coverage": {"uneven": uneven, "gaps": gaps, "not_run_everywhere": nowhere, "worst_case_winner": worst},
           "assumption_effects": effects, "assumptions_untested": untested, "budget_s": a.budget,
           "robust": not why, "not_robust_because": why}
    (dd.BUILD / "decision.json").write_text(json.dumps(out, indent=1, default=str))
    for i, r in enumerate(ranking, 1):
        print(f"{i}. {r['topology']} {r['name'][:40]:<40} score {r['score']:.3f} margin {r['margin']:+.3f} "
              f"knock-outs {len(r['knockouts'])} cost q1000 {r['unit_cost_q1000']}")
    print(f"winner {winner}{' (tie broken by cost)' if tie else ''}; {len(effects)} assumption end(s) tested in "
          f"{time.monotonic() - t0:.0f} s; robust={out['robust']}")
    for x in why:
        print("  not robust:", x)
    print("wrote build/decision.json - record the choice: params/common.yaml meta.chosen + docs/parts/decision.md.j2")


if __name__ == "__main__":
    main()
files/method/repo-template/tools/export_calcs.py (122 wierszy)
#!/usr/bin/env python3
"""Export standalone calculation bundles (make calcs).

  both tiers: deliverables/02-concept/calc/model_<T>.py for every topology
  full tier:  also deliverables/05-calculations/model_<chosen>.py, plus sim/<name>.py and its results file

A bundle embeds ddlib, the model, the checks and the data files verbatim, recomputes every check and model
output with `ddpy model_<T>.py` from its own folder (offline, seconds), writes model_<T>.results.json (status words
of the RUN CONTRACT: NOT RUN, BLOCKED for the engine's UNKNOWN, ERROR) and exits 1 if any status differs from
build/check-results.json. This script runs every bundle once after writing it.
"""
import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

EMBED = ("ddlib.py", "model/*.py", "checks/req_*.py", "params/*.yaml", "requirements/*.yaml", "bom/*.yaml",
         "research/*.yaml", "evidence/*.md", "sim/*.results.json")
BOOT = '''
class _Embedded(importlib.abc.MetaPathFinder, importlib.abc.Loader):
    """Serves ddlib and the model package from FILES."""

    @staticmethod
    def rel(name):
        if name == "ddlib":
            return "ddlib.py"
        return "model/__init__.py" if name == "model" else name.replace(".", "/") + ".py"

    def find_spec(self, name, path=None, target=None):
        if (name in ("ddlib", "model") or name.startswith("model.")) and self.rel(name) in FILES:
            return importlib.util.spec_from_loader(name, self, is_package=name == "model")
        return None

    def create_module(self, spec):
        return None

    def exec_module(self, module):
        module.__file__ = self.rel(module.__name__)
        exec(compile(FILES[module.__file__], module.__file__, "exec"), module.__dict__)


if __name__ == "__main__":
    here = Path(__file__).resolve().parent
    sys.meta_path.insert(0, _Embedded())
    import ddlib as dd
    dd.use_bundle(FILES, here / DELIV_REL)
    P = dd.load_params(TOPOLOGY)
    res = dd.evaluate_all(P, timeout=0)
    out = {rid: {"value": r["value"], "unit": r["unit"], "status": dd.shown(r["status"]), "margin": r["margin"]}
           for rid, r in res.items()}
    out.update({"out." + n: {"value": o["value"], "unit": o["unit"]} for n, o in dd.outputs(P).items()})
    (here / (Path(__file__).stem + ".results.json")).write_text(json.dumps(out, indent=1, default=str))
    diff = [f"{i}: {EXPECTED[i]} -> {r['status']}" for i, r in res.items() if EXPECTED.get(i, r["status"]) != r["status"]]
    print(f"topology {TOPOLOGY}: {len(res)} checks recomputed; " +
          ("all statuses reproduced" if not diff else "MISMATCH " + "; ".join(diff)))
    sys.exit(1 if diff else 0)
'''


def lit(text):
    """A Python literal of text that stays readable (raw triple quotes) when possible."""
    for q in ("'''", '"""'):
        if q not in text and not text.rstrip("\n").endswith("\\"):
            return f"r{q}{text}{q}"
    return repr(text)


def bundle(T, results, deliv_rel):
    files = {rel: dd.read(rel) for pat in EMBED for rel in dd.files(pat)}
    files["_ids.json"] = json.dumps(dd.input_ids())
    name = dd.topologies()[T].get("topology", {}).get("name", T)
    expected = {rid: r["status"] for rid, r in results.items()}
    head = (f'#!/usr/bin/env python3\n"""Standalone recomputation of every requirement check and model output of topology '
            f'{T} ({name}).\nGenerated by design/tools/export_calcs.py from the design repository; the sources below are '
            f'embedded verbatim\n(ddlib = units, parameters, check engine; model/ = physics and cost; checks/ = one per '
            f'requirement).\nRun: ddpy {f"model_{T}.py"}  (offline, seconds) -> model_{T}.results.json; exit 1 if a status '
            f'differs from the design run.\n"""\nimport importlib.abc\nimport importlib.util\nimport json\nimport sys\n'
            f'from pathlib import Path\n\nTOPOLOGY = {T!r}\nDELIV_REL = {deliv_rel!r}\nEXPECTED = '
            f'{json.dumps(expected, indent=1)}\nFILES = {{}}\n')
    body = "".join(f"FILES[{rel!r}] = {lit(text)}\n" for rel, text in files.items())
    return head + body + BOOT


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--tier", choices=["concept", "full"], default="concept")
    a = ap.parse_args()
    data = json.loads((dd.BUILD / "check-results.json").read_text())
    if data.get("inputs_sha") != dd.sources_hash():
        sys.exit("check results are stale: run make check first")
    jobs = [(dd.DELIV / "02-concept" / "calc", T, "../..") for T in sorted(data["topologies"])]
    if a.tier == "full":
        out_dir = dd.DELIV / "05-calculations"
        jobs.append((out_dir, dd.chosen(), ".."))
    bad = 0
    for folder, T, rel in jobs:
        folder.mkdir(parents=True, exist_ok=True)
        path = folder / f"model_{T}.py"
        path.write_text(bundle(T, data["topologies"][T]["results"], rel), encoding="utf-8")
        proc = subprocess.run([sys.executable, path.name], cwd=folder, capture_output=True, text=True, timeout=300)
        print(f"{path.relative_to(dd.RUN)}: exit {proc.returncode} {proc.stdout.strip()[-300:]} {proc.stderr.strip()[-300:]}")
        bad += proc.returncode != 0
    if a.tier == "full":
        for src in sorted((dd.ROOT / "sim").glob("*.py")):
            shutil.copyfile(src, out_dir / src.name)
            res = src.with_suffix(".results.json")
            if res.is_file():
                shutil.copyfile(res, out_dir / res.name)
            else:
                bad += 1
                print(f"sim/{src.name}: no results file - run it from design/sim/ first")
    sys.exit(1 if bad else 0)


if __name__ == "__main__":
    main()
files/method/repo-template/tools/export_params.py (37 wierszy)
#!/usr/bin/env python3
"""Export the chosen topology's parameters for the artefact generators of the full tier (make params).

Writes build/params.json and the copies the generators read next to them:
  deliverables/03-cad/params.json, deliverables/06-electronics/params.json   {name: {value, unit, origin, note}}
  deliverables/07-firmware/include/design_params.h   #define <c_name> for every parameter that has a `c_name`
Generators read these files; they never type a design number themselves.
"""
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

P = dd.load_params()
data = {"topology": P.topology, "params": {}}
lines = ["/* Generated by design/tools/export_params.py from design/params - do not edit. */", "#pragma once", ""]
for n in P.names():
    e = P.entry(n) if isinstance(P.entry(n), dict) else {"value": P.entry(n)}
    data["params"][n] = {k: e.get(k) for k in ("value", "unit", "origin", "note")}
    if e.get("c_name"):
        v = e.get("value")
        lit = f'"{v}"' if isinstance(v, str) else str(int(v)) if isinstance(v, bool) else repr(v)
        lines.append(f"#define {e['c_name']} {lit}  /* {e.get('unit', '')} - {e.get('origin', '')} */")
text = json.dumps(data, indent=1, default=str) + "\n"
dd.BUILD.mkdir(parents=True, exist_ok=True)
(dd.BUILD / "params.json").write_text(text)
for rel in ("03-cad/params.json", "06-electronics/params.json"):
    (dd.DELIV / rel).parent.mkdir(parents=True, exist_ok=True)
    (dd.DELIV / rel).write_text(text)
hdr = dd.DELIV / "07-firmware" / "include" / "design_params.h"
hdr.parent.mkdir(parents=True, exist_ok=True)
hdr.write_text("\n".join(lines) + "\n")
print(f"exported {len(data['params'])} parameters of topology {P.topology}; "
      f"{len(lines) - 3} firmware constants -> 07-firmware/include/design_params.h")
files/method/repo-template/tools/lint.py (267 wierszy)
#!/usr/bin/env python3
"""Repository gate (make lint; make verify = --verify). Exit 1 on errors.

  ddpy design/tools/lint.py [--scope REGEX] [--verify]

lint: parameters (value, unit, origin, note; SOURCE with reference and date; ASSUMPTION with a range; no silent
override of a common parameter), requirements (every INPUT id present, sense/target/unit consistent), checks (one
per requirement, compute(P, R), no status literals, no typed numbers), model (selftests, numbers in code outside
selftests, stubs left), templates and evidence (no typed quantities outside Jinja expressions), research data.
--verify: deliverables equal the last render (no hand edits), the render is not stale, no [[...]] markers or raw
Jinja left; stale explorations are warned. --scope keeps only messages matching REGEX (a ticket's files/ids).
"""
import argparse
import ast
import hashlib
import json
import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

E, W = [], []
DATE = re.compile(r"\d{4}-\d{2}(-\d{2})?")
REF = re.compile(r"https?://|INPUT/|datasheet|catalog|standard|\b(EN|IEC|ISO|UL)\s?\d|lcsc|research/", re.I)
UNITS = (r"(?:mm|cm|km|µm|m|kg|mg|g|kW|mW|W|kWh|Wh|kV|mV|V|mA|µA|A|mAh|Ah|kHz|MHz|GHz|Hz|°C|degC|K|ms|s|min|h|%|"
         r"dB\(A\)|dBA|dB|mL|ml|L|m³|m²|kPa|MPa|Pa|bar|Nm|N|lm|lx|cd|EUR|USD|PLN|GBP|rpm|deg|°)")
QTY = re.compile(r"(?<![\w.\-/])[-+]?\d+(?:[.,]\d+)?\s?" + UNITS + r"(?!\w)|[€$£]\s?\d")
JINJA = re.compile(r"\{\{.*?\}\}|\{%.*?%\}|\{#.*?#\}", re.S)
SOURCED = re.compile(r"\bSOURCE\b.*(https?://|INPUT/|\b(EN|IEC|ISO|UL)\s?\d)")   # an external fact with its reference
MARK = re.compile(r"\[\[(?:WRITE|MISSING|RENDER ERROR)[^\]]*\]\]")
STATUS_LIT = re.compile(r"[\"'](PASS|PARTIAL|FAIL)[\"']")
RESERVED = {"topology", "info", "features", "has", "entry", "names", "with_"}
MODEL_OK = {0, 1, 2, 3, 4, 0.5, 10, 100, 1000, 60, 3600, 1e3, 1e-3, 1e6, 1e-6}


def numbers(tree, allowed):
    """Lines with typed numbers, outside selftest_* functions (known-answer constants belong there)."""
    tests = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name.startswith("selftest_")]
    skip = {line for f in tests for line in range(f.lineno, (f.end_lineno or f.lineno) + 1)}
    return sorted(n.lineno for n in ast.walk(tree) if isinstance(n, ast.Constant) and type(n.value) in (int, float)
                  and abs(n.value) not in allowed and n.lineno not in skip)


def lint_params():
    tops = dd.topologies()
    if len(tops) < 3:
        W.append(f"params: {len(tops)} topology file(s); the method asks for 3 (2 only with a recorded reason)")
    meta = dd.meta()
    if meta.get("chosen") and meta["chosen"] not in tops:
        E.append(f"params/common.yaml: meta.chosen {meta['chosen']!r} is not a topology")
    for k in ("product", "currency"):
        if not meta.get(k):
            W.append(f"params/common.yaml: meta.{k} is empty")
    shared = (dd.common().get("params") or {}) if "params/common.yaml" in dd.files("params/*.yaml") else {}
    for t, d in tops.items():
        for n, e in (d.get("params") or {}).items():
            if n in shared and "override:" not in str((e or {}).get("note", "") if isinstance(e, dict) else ""):
                W.append(f"params/{t}.yaml: {n} shadows params/common.yaml:{n} - rename it, or say 'override: <reason>' in its note")
    for rel in dd.files("params/*.yaml"):
        try:
            d = dd.yload(rel)
        except Exception as e:
            E.append(f"{rel}: YAML error {e}")
            continue
        if Path(rel).name != "common.yaml":
            t = d.get("topology") or {}
            if not t.get("name") or not t.get("principle"):
                E.append(f"{rel}: topology.name and topology.principle are required")
        for n, e in (d.get("params") or {}).items():
            where = f"{rel}: {n}"
            if n in RESERVED:
                E.append(f"{where}: reserved name, rename the parameter")
            if not isinstance(e, dict) or "value" not in e:
                E.append(f"{where}: needs value, unit, origin, note")
                continue
            u = str(e.get("unit") or "")
            if u and not dd.norm_unit(u)[0]:
                W.append(f"{where}: unit '{u}' not understood by pint (treated as a plain count)")
            if e.get("origin") not in dd.ORIGINS:
                E.append(f"{where}: origin {e.get('origin')!r} not in {dd.ORIGINS}")
            note = str(e.get("note") or "")
            if not note.strip():
                E.append(f"{where}: note (reason or source) is empty")
            if e.get("origin") == "SOURCE" and not REF.search(note):
                E.append(f"{where}: SOURCE note needs a reference (URL, document, INPUT/...)")
            elif e.get("origin") == "SOURCE" and not DATE.search(note) and "INPUT/" not in note:
                W.append(f"{where}: SOURCE note has no date read")
            num = isinstance(e["value"], (int, float)) and not isinstance(e["value"], bool)
            rng = e.get("range")
            if rng is not None:
                if not (isinstance(rng, list) and len(rng) == 2 and all(isinstance(x, (int, float)) for x in rng) and rng[0] < rng[1]):
                    E.append(f"{where}: range must be [lo, hi] with lo < hi")
                elif num and not rng[0] <= e["value"] <= rng[1]:
                    W.append(f"{where}: value outside its range")
            elif num and (e.get("origin") == "ASSUMPTION" or e.get("kind") == "design"):
                W.append(f"{where}: {e.get('origin') if e.get('origin') == 'ASSUMPTION' else 'design variable'} without range (no sensitivity / exploration)")


def lint_requirements():
    reqs, seen = dd.requirements(), {}
    for rel in dd.files("requirements/*.yaml"):
        for r in dd.yload(rel).get("requirements") or []:
            if r.get("id") in seen:
                E.append(f"{rel}: duplicate requirement id {r.get('id')} (also in {seen[r['id']]})")
            seen[r.get("id")] = rel
    for rid in dd.input_ids():
        if rid not in reqs:
            E.append(f"requirement {rid} of INPUT/requirements.md has no entry in requirements/*.yaml")
    for rid, r in reqs.items():
        where = f"{r['_file']}: {rid}"
        sense = r.get("sense")
        if sense not in dd.SENSES:
            E.append(f"{where}: sense {sense!r} not in {dd.SENSES}")
        elif sense in ("<=", ">="):
            vals = [r.get("target"), r.get("threshold")]
            if not any(isinstance(v, (int, float)) and not isinstance(v, bool) for v in vals):
                E.append(f"{where}: numeric sense needs a numeric target or threshold")
        elif sense == "range" and not all(v is None or (isinstance(v, list) and len(v) == 2) for v in (r.get("target"), r.get("threshold"))):
            E.append(f"{where}: range sense needs [lo, hi] bounds")
        if r.get("kind", "given") == "given" and not (str(r.get("interpretation") or "").strip() and str(r.get("consequence") or "").strip()):
            W.append(f"{where}: interpretation/consequence empty")
        if rid not in dd.input_ids() and not re.match(r"^DR-[A-Z0-9]+-\d+$", rid):
            W.append(f"{where}: derived requirement ids look like DR-<AREA>-<nn>")
        if rid not in dd.input_ids() and not r.get("parents"):
            W.append(f"{where}: derived requirement without parents")
        if f"checks/req_{rid}.py" not in dd.files("checks/req_*.py"):
            (E if rid in dd.input_ids() else W).append(f"{where}: no check checks/req_{rid}.py")
    for rel in dd.files("checks/req_*.py"):
        rid = Path(rel).stem[4:]
        if rid not in reqs:
            E.append(f"{rel}: no requirement {rid} in requirements/*.yaml")
        try:
            tree = ast.parse(dd.read(rel))
        except SyntaxError as e:
            E.append(f"{rel}: syntax error {e}")
            continue
        if not any(isinstance(n, ast.FunctionDef) and n.name == "compute" and len(n.args.args) == 2 for n in tree.body):
            E.append(f"{rel}: must define compute(P, R)")
        if STATUS_LIT.search(dd.read(rel)):
            E.append(f"{rel}: status literal found - a check returns a value; ddlib decides the status")
        lines = numbers(tree, {0, 1})
        if lines:
            W.append(f"{rel}: numbers typed in a check (lines {lines[:5]}) - move them to the requirement's conditions or params")
        if "scaffold stub" in (ast.get_docstring(tree) or ""):
            W.append(f"{rel}: still the scaffold stub - refine it and its docstring")


def lint_model():
    tops = dd.topologies()
    for t in tops:
        if f"model/topo_{t}.py" not in dd.files("model/*.py"):
            W.append(f"model/topo_{t}.py missing (topology {t} not modelled)")
    for rel in dd.files("model/*.py"):
        if Path(rel).name.startswith("_"):
            continue
        try:
            tree = ast.parse(dd.read(rel))
        except SyntaxError as e:
            E.append(f"{rel}: syntax error {e}")
            continue
        if not any(isinstance(n, ast.FunctionDef) and n.name.startswith("selftest_") for n in tree.body):
            W.append(f"{rel}: no selftest_* known-answer test")
        stubs = [n.name for n in tree.body if isinstance(n, ast.FunctionDef)
                 and any(isinstance(d, ast.Name) and d.id == "stub" for d in n.decorator_list)]
        if stubs:
            W.append(f"{rel}: {len(stubs)} scaffold stub(s) still raise Unknown: {', '.join(stubs[:8])}")
        lines = numbers(tree, MODEL_OK)
        if lines:
            W.append(f"{rel}: {len(lines)} typed number(s) (lines {lines[:6]}) - design numbers belong in params/")


def lint_templates():
    for rel in dd.files("docs/*.j2") + dd.files("docs/parts/*.j2") + dd.files("evidence/*.md"):
        text = JINJA.sub(lambda m: "\n" * m.group(0).count("\n"), dd.read(rel))
        for i, line in enumerate(text.splitlines(), 1):
            hits = [] if SOURCED.search(line) else QTY.findall(line)
            if hits:
                E.append(f"{rel}:{i}: typed quantity {hits[:3]} - quote it with p()/o()/c() from params or the model")


def lint_research():
    if "research/benchmark.yaml" not in dd.files("research/*.yaml"):
        W.append("research/benchmark.yaml missing (README 'Research data')")
        return
    try:
        raw = dd.yload("research/benchmark.yaml")
    except Exception as e:
        E.append(f"research/benchmark.yaml: YAML error {e}")
        return
    items = raw if isinstance(raw, list) else raw.get("products") if isinstance(raw, dict) else None
    if not isinstance(items, list) or not all(isinstance(b, dict) for b in items):
        E.append("research/benchmark.yaml: needs products: [{name, maker, price, specs, weak_points, url, read}]")
        return
    if len(items) < 5:
        W.append(f"research/benchmark.yaml: {len(items)} product(s); five or more comparable products expected")
    for i, b in enumerate(items, 1):
        if not str(b.get("url", "")).startswith("http") or not DATE.search(str(b.get("read", ""))):
            W.append(f"research/benchmark.yaml: product {i} ({b.get('name', '?')}) lacks url or date read")


def verify():
    rp = dd.BUILD / "rendered.json"
    if not rp.is_file():
        E.append("no build/rendered.json: run make docs")
        return
    rec = json.loads(rp.read_text())
    for rel, sha in rec["files"].items():
        f = dd.DELIV / rel
        if not f.is_file():
            E.append(f"deliverables/{rel}: rendered file is missing")
        elif hashlib.sha256(f.read_bytes()).hexdigest() != sha:
            E.append(f"deliverables/{rel}: changed after the render (hand edit?) - fix the source and make docs")
    if rec.get("inputs_sha") != dd.sources_hash():
        E.append("render is stale: params/requirements/checks/model/bom changed - make check decide calcs docs")
    if rec.get("docs_sha") != dd.docs_hash():
        E.append("render is stale: templates/parts/evidence/research changed - make docs")
    cr = dd.BUILD / "check-results.json"
    if cr.is_file() and json.loads(cr.read_text()).get("generated") != rec.get("check_results"):
        E.append("render is stale: check results are newer - make docs")
    dp = dd.BUILD / "decision.json"
    if dp.is_file() and json.loads(dp.read_text()).get("generated") != rec.get("decision"):
        E.append("render is stale: decision.json is newer - make docs")
    for f in sorted(dd.BUILD.glob("explore-*.json")):
        x = json.loads(f.read_text())
        try:
            stale = x.get("context_sha") != dd.context_hash(x.get("topology"), [v["name"] for v in x.get("vars", [])])
        except (Exception, SystemExit):
            stale = True
        if stale:
            W.append(f"build/{f.name}: model or other parameters of {x.get('topology')} changed since this sweep "
                     f"(concept.md marks it indicative; re-run its explore script to refresh)")
    for f in sorted(dd.DELIV.rglob("*")):
        if f.suffix in (".md", ".csv", ".json") and f.is_file():
            text = f.read_text(encoding="utf-8", errors="replace")
            marks = MARK.findall(text) + (["raw Jinja"] if f.suffix == ".md" and re.search(r"\{\{|\{%", text) else [])
            if marks:
                E.append(f"deliverables/{f.relative_to(dd.DELIV)}: {len(marks)} unfinished marker(s), e.g. {marks[:2]}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--scope", help="regex: keep only messages that match")
    ap.add_argument("--verify", action="store_true")
    a = ap.parse_args()
    if a.verify:
        verify()
    else:
        lint_params()
        lint_requirements()
        lint_model()
        lint_templates()
        lint_research()
    errs = [m for m in E if not a.scope or re.search(a.scope, m)]
    warns = [m for m in W if not a.scope or re.search(a.scope, m)]
    for m in warns:
        print("WARN ", m)
    for m in errs:
        print("ERROR", m)
    print(f"lint{' --verify' if a.verify else ''}: {len(errs)} error(s), {len(warns)} warning(s)")
    return 1 if errs else 0


if __name__ == "__main__":
    sys.exit(main())
files/method/repo-template/tools/render.py (436 wierszy)
#!/usr/bin/env python3
"""Render deliverables/ from the repository. MAINTAINER ONLY (it writes deliverables/REPORT.md).

  ddpy design/tools/render.py --tier concept|full [--force]      (make docs)

Renders the templates and copies listed in docs/map.yaml, generates 01-requirements/compliance.csv from the
check results of the chosen topology, 09-bom-cost/cost.json (+ bom.csv at the full tier) from bom/<T>.yaml,
and manifest.json; records build/rendered.json for `lint.py --verify`. Refuses stale check results unless --force.
Deliverables use the RUN CONTRACT status words: the engine's UNKNOWN and MISSING read NOT RUN, ERROR reads BLOCKED.
"""
import argparse
import csv
import hashlib
import io
import json
import shutil
import sys
from pathlib import Path

import jinja2
import yaml

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

WORD = {"PASS": "met", "PARTIAL": "partly", "FAIL": "not_met"}       # NOT RUN and BLOCKED: not_addressed
KINDS = {".md": "document", ".csv": "table", ".json": "data", ".yaml": "data", ".png": "figure", ".svg": "drawing",
         ".pdf": "drawing", ".py": "calculation", ".step": "cad", ".stp": "cad", ".glb": "3d-model", ".dxf": "drawing",
         ".kicad_pro": "electronics", ".kicad_sch": "electronics", ".kicad_pcb": "electronics", ".ini": "firmware"}
ap = argparse.ArgumentParser()
ap.add_argument("--tier", choices=["concept", "full"], default="concept")
ap.add_argument("--force", action="store_true")
a = ap.parse_args()
D, written, descs, errors = dd.DELIV, {}, {}, []

rp = dd.BUILD / "check-results.json"
if not rp.is_file():
    sys.exit("build/check-results.json missing: run make check first")
data = json.loads(rp.read_text())
if data.get("inputs_sha") != dd.sources_hash() and not a.force:
    sys.exit("check results are stale (sources changed since make check): run make check, then render")
dp = dd.BUILD / "decision.json"
decision = json.loads(dp.read_text()) if dp.is_file() else None
T = dd.chosen()
results = {t: d["results"] for t, d in data["topologies"].items()}
outs = {t: d.get("outputs", {}) for t, d in data["topologies"].items()}
R = dd.requirements()
fmt_entry = dd.fmt_param


def entries(params):
    out = []
    for n, e in (params or {}).items():
        e = e if isinstance(e, dict) else {"value": e}
        out.append({"name": n, "text": fmt_entry(e), "origin": e.get("origin", "?"), "note": e.get("note", ""),
                    "kind": e.get("kind", "fact"), "range": e.get("range"), "unit": e.get("unit", "")})
    return out


def safe(fn, default=None):
    try:
        return fn()
    except (Exception, SystemExit):
        return default


common = dd.yload("params/common.yaml") if "params/common.yaml" in dd.files("params/*.yaml") else {}
topos = [{"id": t, "name": d.get("topology", {}).get("name", t), "principle": d.get("topology", {}).get("principle", ""),
          "features": d.get("features") or [], "entries": entries(d.get("params"))} for t, d in sorted(dd.topologies().items())]
cost = {t["id"]: safe(lambda t=t: dd.rollup(dd.load_params(t["id"]))) for t in topos}


def effect_text(x):
    ch = ", ".join(f"{c['topology']} {c['id']} {dd.shown(c['from'])} -> {dd.shown(c['to'])}" for c in x["changes"])
    txt = f"{x['end']} ({fmt_entry({'value': x['value'], 'unit': x['unit']})}): {ch or 'no status change'}"
    return txt + (f", winner becomes {x['winner']}" if x.get("winner") not in (None, decision["winner"]) else "")


effects, untested = {}, (decision or {}).get("assumptions_untested", [])
for x in (decision or {}).get("assumption_effects", []):
    effects.setdefault((x["scope"], x["param"]), []).append(effect_text(x))
assumptions = []
for scope, params in [("common", common.get("params"))] + [(t, d.get("params")) for t, d in sorted(dd.topologies().items())]:
    for e in entries(params):
        if e["origin"] != "ASSUMPTION":
            continue
        if e["kind"] == "design":
            eff = "design variable (chosen; its range is explored in 02-concept, not an uncertainty)"
        elif not e["range"]:
            eff = "not tested (no range)"
        elif decision is None:
            eff = "not tested yet (make decide)"
        elif (scope, e["name"]) in effects:
            eff = "; ".join(effects[(scope, e["name"])])
        elif any(u.startswith(f"{e['name']} ({scope},") for u in untested):
            eff = "not tested (time budget of make decide)"
        else:
            eff = "not used (every topology overrides it)" if scope == "common" else "not tested (re-run make decide)"
        assumptions.append(dict(e, scope=scope, effects=eff))
explore = []
for f in sorted(dd.BUILD.glob("explore-*.json")):
    x = json.loads(f.read_text())
    x.pop("points", None)
    x["stale"] = safe(lambda: x.get("context_sha") != dd.context_hash(x.get("topology"), [v["name"] for v in x["vars"]]), True)
    explore.append(x)
bench = []
if "research/benchmark.yaml" in dd.files("research/*.yaml"):
    try:
        raw = dd.yload("research/benchmark.yaml")
        bench = raw if isinstance(raw, list) else raw.get("products") if isinstance(raw, dict) else None
        if not isinstance(bench, list) or not all(isinstance(b, dict) for b in bench):
            raise ValueError(f"top level {list(raw) if isinstance(raw, dict) else type(raw).__name__}")
    except Exception as e:
        errors.append(f"research/benchmark.yaml: {e}; expected products: [{{name, maker, price, specs: {{...}}, "
                      f"weak_points: [...], url, read}}] (README 'Research data')")
        bench = [b for b in bench if isinstance(b, dict)] if isinstance(bench, list) else []
blocked = (dd.yload("blocked.yaml").get("blocked") or []) if (dd.ROOT / "blocked.yaml").is_file() else []


def p(name, t=None, label=True):
    """Parameter with unit and origin label, e.g. '900 W (ASSUMPTION)'; label=False for tables with an origin column."""
    P = safe(lambda: dd.load_params(t or T))
    if P is None or not P.has(name):
        return f"[[MISSING p({name})]]"
    e = P.entry(name)
    return fmt_entry(e) + (f" ({e.get('origin', '?')})" if label and isinstance(e, dict) else "")


def o(name, t=None, label=True):
    """Model output with unit and label, e.g. '1.2 kW (CALCULATION `model/topo_A.py:power`)'."""
    x = outs.get(t or T, {}).get(name) or {}
    if x.get("value") is not None:
        txt, where = dd.fmt(x["value"], x["unit"]), x.get("where", "")
    else:
        try:
            P = dd.load_params(t or T)
            txt, where = dd.fmt(dd.need(P, name)), dd.find(P.topology, name)[1]
        except (Exception, SystemExit) as e:
            return f"[[MISSING o({name}): {e}]]"
    return f"{txt} (CALCULATION `{where}`)" if label else txt


def c(rid, t=None):
    """Check result; status in RUN CONTRACT words (engine = the check engine's own status)."""
    r = results.get(t or T, {}).get(rid) or dd._blank(rid, t or T, "MISSING", "not checked")
    return dict(r, status=dd.shown(r["status"]), engine=r["status"])


def val(rid, t=None, label=False):
    r = c(rid, t)
    if isinstance(r["value"], (int, float)):
        txt = dd.fmt(r["value"], r["unit"])
        return f"{txt} (CALCULATION `checks/req_{rid}.py`)" if label else txt
    return r["value"] or ""


def cell(rid, t=None):
    return f"{c(rid, t)['status']} {val(rid, t)}".strip()


def word(rid, t=None):
    return WORD.get(c(rid, t)["engine"], "not_addressed")


def req(rid, what="threshold", label=True):
    """A requirement's target, threshold or verification condition with unit and origin label."""
    r = R.get(rid)
    if r is None:
        return f"[[MISSING req({rid})]]"
    if what in ("target", "threshold"):
        v, u = r.get(what), r.get("unit_label") or r.get("unit") or ""
        if v is None:
            return f"[[MISSING req({rid}, {what})]]"
        num = lambda x: dd.fmt(x, u) if isinstance(x, (int, float)) and not isinstance(x, bool) else str(x)
        txt = " … ".join(num(x) for x in v) if isinstance(v, list) else num(v)
        src = "SOURCE INPUT/requirements.md" if rid in dd.input_ids() else f"{r.get('origin') or 'ASSUMPTION'}, derived requirement {rid}"
    else:
        cnd = (r.get("conditions") or {}).get(what)
        if cnd is None:
            return f"[[MISSING req({rid}, {what})]]"
        txt = fmt_entry(cnd)
        src = f"{(cnd.get('origin') if isinstance(cnd, dict) else None) or 'ASSUMPTION'}, verification condition of {rid}"
    return f"{txt} ({src})" if label else txt


def note(rid):
    """compliance.csv note: what the status rests on."""
    r, src = c(rid), f"checks/req_{rid}.py"
    via = ", ".join(x for x in r.get("evidence", []) if ".py:" in x)
    if r["engine"] in dd.COMPUTED and (isinstance(r["value"], (int, float)) or via):
        txt = f"{r['status']} (predicted by analysis, CALCULATION: {src}{' with ' + via if via else ''}; topology {T})"
        if isinstance(r.get("margin"), (int, float)):
            txt += f"; margin {dd.fmt(r['margin'], r['unit'])} to the {'threshold' if R.get(rid, {}).get('threshold') is not None else 'target'}"
        return txt
    if r["engine"] in dd.COMPUTED:
        return f"{r['status']} (inspection of the design decisions of topology {T} by {src}: {r['value']})"
    if r["engine"] == "ERROR":
        return f"BLOCKED: {src} fails ({r.get('reason', '')}); nothing is claimed"
    txt = f"NOT RUN: {r.get('reason') or 'no check'}; nothing is claimed"
    return txt + (f"; argument in 02-concept/evidence/{rid}.md" if (dd.ROOT / "evidence" / f"{rid}.md").is_file() else "")


ctx = {"tier": a.tier, "T": T, "chosen": next((x for x in topos if x["id"] == T), {"id": T, "name": T}),
       "product": (common.get("meta") or {}).get("product") or "the product", "generated": data["generated"],
       "ids": dd.input_ids(), "reqs": [dict(R[i], id=i) for i in dd.ordered_ids() if i in R], "R": R,
       "topos": topos, "common": entries(common.get("params")), "results": results, "outputs": outs,
       "scores": {t: d.get("summary", {}) for t, d in data["topologies"].items()}, "decision": decision,
       "explore": explore, "assumptions": assumptions, "cost": cost, "benchmark": bench,
       "blocked": blocked, "selftests": data.get("selftests", []), "currency": (common.get("meta") or {}).get("currency", ""),
       "not_applicable": [(k, v) for rel in dd.files("requirements/*.yaml") for k, v in (dd.yload(rel).get("areas_not_applicable") or {}).items()]}
env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(dd.ROOT / "docs")), undefined=jinja2.StrictUndefined,
                         trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True)


def part(name, required=True):
    if not (dd.ROOT / "docs" / "parts" / f"{name}.md.j2").is_file():
        return f"[[WRITE: docs/parts/{name}.md.j2]]" if required else ""
    try:
        return env.get_template(f"parts/{name}.md.j2").render(**ctx).strip()
    except Exception as e:
        return f"[[RENDER ERROR in docs/parts/{name}.md.j2: {type(e).__name__}: {e}]]"


def deliv_files():
    return sorted(str(f.relative_to(D)) for f in D.rglob("*") if f.is_file())


def cells(rid, label=False):
    return ("; " if label else " | ").join(f"{t['id']}: {cell(rid, t['id'])}" if label else cell(rid, t["id"]) for t in topos)


def conds(r):
    return ", ".join(f"{k} = {fmt_entry(v)}" for k, v in (r.get("conditions") or {}).items())


def bounds(r):
    u = r.get("unit_label") or r.get("unit") or ""
    f = lambda v: "—" if v is None else (dd.fmt(v, u) if isinstance(v, (int, float)) and not isinstance(v, bool) else str(v))
    return f"{r.get('sense')} {f(r.get('target'))} / {f(r.get('threshold'))}" if r.get("sense") in ("<=", ">=", "range") \
        else f"{r.get('raw_target') or r.get('target')} / {r.get('raw_threshold') or r.get('threshold')}"


def outline(t):
    return "; ".join(f"{n} = {dd.fmt(x['value'], x['unit'])}" for n, x in outs.get(t, {}).items() if x.get("value") is not None)


def counts(cn):
    agg = {}
    for k, v in (cn or {}).items():
        agg[dd.shown(k)] = agg.get(dd.shown(k), 0) + v
    return ", ".join(f"{k} {v}" for k, v in sorted(agg.items()))


def scoreline(t):
    s = ctx["scores"].get(t) or {}
    return f"{counts(s.get('counts'))}; weighted score {s.get('score', 0):.3f}; knock-outs: {', '.join(s.get('knockouts') or []) or 'none'}"


def money(d):
    return " | ".join(dd.fmt(d[q]) for q in dd.QS)


def pt(x, which):
    units = {v["name"]: v.get("unit", "") for v in x.get("vars", [])}
    vals = x["best"]["x"] if which == "best" else x.get("current", {})
    return ", ".join(f"{k} = {dd.fmt(v, dd.norm_unit(units.get(k))[1])}" for k, v in vals.items())


def now(x):
    """Design values of a sweep's variables as the parameters hold them now (not at sweep time)."""
    return ", ".join(f"{v['name']} = {p(v['name'], x.get('topology'))}" for v in x.get("vars", []))


def versus(t, w=None):
    """Why topology t ranks below w (default: the winner), from the check results."""
    w = w or (decision or {}).get("winner") or T
    rk = {r["topology"]: r for r in (decision or {}).get("ranking", [])}
    a_, b_ = rk.get(t, {}), rk.get(w, {})
    s = lambda u, rid: dd.STATUS_SCORE.get(results.get(u, {}).get(rid, {}).get("status"), 0.0)
    pr = lambda rid: -float((R.get(rid) or {}).get("priority") or 1)
    worse = sorted((rid for rid in dd.ordered_ids() if s(t, rid) < s(w, rid)), key=pr)
    better = sorted((rid for rid in dd.ordered_ids() if s(t, rid) > s(w, rid)), key=pr)
    dif = lambda ids: ", ".join(f"{rid} ({c(rid, t)['status']} vs {c(rid, w)['status']})" for rid in ids[:5])
    out = [f"knock-outs: {', '.join(a_.get('knockouts') or []) or 'none'}",
           f"weighted score {a_.get('score', 0):.3f} against {b_.get('score', 0):.3f} of {w}"]
    out += [f"worse than {w} on {dif(worse)}"] if worse else []
    out += [f"better on {dif(better)}"] if better else []
    if a_.get("unit_cost_q1000") is not None and b_.get("unit_cost_q1000") is not None:
        out.append(f"unit cost at 1000 {dd.fmt(a_['unit_cost_q1000'], ctx['currency'])} against "
                   f"{dd.fmt(b_['unit_cost_q1000'], ctx['currency'])} (CALCULATION)")
    return "; ".join(out)


def tight(n=5, t=None):
    """The n smallest relative margins of computed numeric checks (risk candidates)."""
    rs = [c(rid, t) for rid in dd.ordered_ids()]
    return sorted((r for r in rs if r["engine"] in dd.COMPUTED and isinstance(r.get("margin_rel"), (int, float))),
                  key=lambda r: r["margin_rel"])[:n]


def filemap():
    groups = {}
    for f in deliv_files():
        groups.setdefault(f.split("/")[0] if "/" in f else ".", []).append(f)
    return [(g, len(v), v[:4]) for g, v in sorted(groups.items())]


def kv(d):
    if isinstance(d, dict):
        return "; ".join(f"{k}: {v}" for k, v in d.items())
    return "; ".join(map(str, d)) if isinstance(d, list) else str(d or "")


def lst(x):
    return [str(v) for v in x] if isinstance(x, list) else [str(x)] if x else []


def modsrc():
    return [(rel, dd.read(rel)) for rel in dd.files("model/*.py") if dd.read(rel).strip()]


def words(x):
    """JSON data with the RUN CONTRACT status words (copies of check-results.json and decision.json)."""
    if isinstance(x, list):
        return [words(v) for v in x]
    if not isinstance(x, dict):
        return x
    out = {}
    for k, v in x.items():
        if k in dd.SHOWN and isinstance(v, int):                       # a count keyed by engine status
            out[dd.shown(k)] = out.get(dd.shown(k), 0) + v
        else:
            out[k] = dd.shown(v) if k in ("status", "from", "to") and isinstance(v, str) else words(v)
    return out


env.globals.update(p=p, o=o, c=c, val=val, cell=cell, cells=cells, word=word, req=req, part=part, fmt=dd.fmt,
                   src=dd.check_source, conds=conds, bounds=bounds, outline=outline, counts=counts, scoreline=scoreline,
                   money=money, pt=pt, now=now, versus=versus, tight=tight, filemap=filemap, kv=kv, lst=lst,
                   modsrc=modsrc, deliv_files=deliv_files, **ctx)


def write(rel, text, desc=""):
    path = D / rel
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    written[rel] = hashlib.sha256(text.encode("utf-8")).hexdigest()
    if desc:
        descs[rel] = desc


m = yaml.safe_load((dd.ROOT / "docs" / "map.yaml").read_text())
for cp in m.get("copy") or []:
    if a.tier not in cp.get("tiers", ["concept", "full"]):
        continue
    for src in sorted(dd.ROOT.glob(cp["src"])):
        rel = f"{cp['dest'].rstrip('/')}/{src.name}"
        if cp.get("render"):
            try:
                write(rel, env.from_string(src.read_text(encoding="utf-8")).render(**ctx), cp.get("desc", ""))
            except Exception as e:
                errors.append(f"{src.relative_to(dd.ROOT)}: {type(e).__name__}: {e}")
        elif cp.get("words"):
            write(rel, json.dumps(words(json.loads(src.read_text())), indent=1, default=str) + "\n", cp.get("desc", ""))
        else:
            (D / rel).parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(src, D / rel)
            written[rel] = hashlib.sha256((D / rel).read_bytes()).hexdigest()
            descs[rel] = cp.get("desc", "")

# generated tables
calc_dir = "02-concept/calc" if a.tier == "concept" else "05-calculations"
buf = io.StringIO()
w = csv.writer(buf, lineterminator="\n")
w.writerow(["req_id", "status", "value", "unit", "evidence", "note"])
for rid in dd.input_ids():
    r = c(rid)
    ev = ["01-requirements/requirements.md", "02-concept/check-results.md", f"{calc_dir}/model_{T}.results.json"]
    if (dd.ROOT / "evidence" / f"{rid}.md").is_file():
        ev.append(f"02-concept/evidence/{rid}.md")
    value_txt = dd.fmt(r["value"]) if isinstance(r["value"], (int, float)) else (r["value"] or "")
    w.writerow([rid, word(rid), value_txt, r["unit"], "; ".join(ev), note(rid)])
write("01-requirements/compliance.csv", buf.getvalue(), "Compliance per requirement id, generated from the check results")

roll = cost.get(T)
if roll:
    rnd = lambda d: {q: round(v, 2) for q, v in d.items()}
    cj = {"currency": roll["currency"], "bom": rnd(roll["bom_cost"]), "other": {k: rnd(v) for k, v in roll["other"].items()},
          "unit_cost": rnd(roll["unit_cost"])}
    write("09-bom-cost/cost.json", json.dumps(cj, indent=1) + "\n", f"Cost roll-up of topology {T} (bom/{T}.yaml)")
    if a.tier == "full":
        buf = io.StringIO()
        w = csv.writer(buf, lineterminator="\n")
        w.writerow(["item", "qty", "description", "manufacturer", "mpn", "supplier", "supplier_part", "url",
                    "unit_price_1", "unit_price_100", "unit_price_1000", "currency", "notes"])
        for ln in roll["lines"]:
            w.writerow([ln.get("item", ""), dd.fmt(ln["qty"]), ln.get("description", ""), ln.get("manufacturer", ""),
                        ln.get("mpn", ""), ln.get("supplier", ""), ln.get("supplier_part", ""), ln.get("url", ""),
                        *ln["price"], ln.get("currency") or roll["currency"],
                        f"{ln.get('origin', 'ASSUMPTION')}: {ln.get('notes', '')}".strip()])
        write("09-bom-cost/bom.csv", buf.getvalue(), "Bill of materials (generated from bom yaml)")
elif a.tier == "full":
    errors.append(f"bom/{T}.yaml missing or incomplete: no cost.json / bom.csv")

for item in m.get("render") or []:      # REPORT last in map.yaml, so its file map sees the other documents
    if a.tier not in item.get("tiers", ["concept", "full"]):
        continue
    try:
        write(item["out"], env.get_template(item["template"]).render(), item.get("desc", ""))
    except Exception as e:
        errors.append(f"docs/{item['template']}: {type(e).__name__}: {e}")


def mm(name, unit):
    return safe(lambda: round(float(dd.need(dd.load_params(T), name).to(unit).magnitude), 3))


dims = [mm(n, "mm") for n in ("length", "width", "height")]
uc = roll["unit_cost"] if roll else None
retail = safe(lambda: dd.need(dd.load_params(T), "retail_price"))
kf = {"dimensions_mm": dims if any(x is not None for x in dims) else None, "mass_kg": mm("mass", "kg"),
      "power_w": mm("power", "W"),
      "unit_cost": {"currency": roll["currency"], **{q: round(uc[q], 2) for q in dd.QS}} if uc else None,
      "retail_price": {"currency": f"{retail.units:~P}", "value": round(float(retail.magnitude), 2)} if retail is not None else None}
mf = [{"path": rel, "kind": KINDS.get(Path(rel).suffix.lower(), "file"),
       "desc": descs.get(rel) or f"{KINDS.get(Path(rel).suffix.lower(), 'file')} in {Path(rel).parent}"}
      for rel in deliv_files() if rel != "manifest.json"]
write("manifest.json", json.dumps({"tier": a.tier, "files": mf, "key_figures": kf, "blocked": blocked}, indent=1) + "\n")
(dd.BUILD / "rendered.json").write_text(json.dumps({"tier": a.tier, "inputs_sha": data.get("inputs_sha"),
                                                     "docs_sha": dd.docs_hash(), "check_results": data["generated"],
                                                     "decision": (decision or {}).get("generated"), "files": written}, indent=1))
print(f"rendered {len(written)} files into deliverables/ (topology {T}, tier {a.tier})")
for e in errors:
    print("ERROR", e)
sys.exit(1 if errors else 0)
files/method/repo-template/tools/req_import.py (126 wierszy)
#!/usr/bin/env python3
"""Import INPUT/requirements.md into requirements/<NN>-<group>.yaml, one file per '## ' section (make reqs).

Parses headings with requirement ids and 'Key: value' bullets (Metric, Target, Threshold, Priority/Weight,
Checked by/Verification). Numeric targets become sense/target/threshold/unit; anything else becomes an 'argued'
requirement with the raw text kept. Never overwrites existing files unless --force. Review the printed list:
every 'argued' or unparsed bound needs a careful look by the ticket that owns the group.
"""
import re
import sys
from pathlib import Path

import yaml

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

CMP = {"≤": "<=", "<=": "<=", "<": "<=", "=<": "<=", "≥": ">=", ">=": ">=", ">": ">=", "=>": ">="}
BOUND = re.compile(r"^(≤|≥|<=|>=|=<|=>|<|>)?\s*([-+]?\d{1,3}(?:,\d{3})+(?:\.\d+)?|[-+]?\d+(?:[.,]\d+)?)\s*"
                   r"([^\s,;·(]*(?:\([A-Za-z]\))?)\s*(.*)$")
THOUSANDS = re.compile(r"[-+]?\d{1,3}(?:,\d{3})+(?:\.\d+)?")      # 1,440 = 1440; 1,5 = 1.5
KV = re.compile(r"^[-*]\s*\**([A-Za-z][\w /()-]{1,30}?)\**\s*:\s*\**\s*(.+)$")
PARTS = re.compile(r"(Target|Threshold)\s*:\s*(.*?)(?=\s*[·;|]\s*(?:Target|Threshold)\s*:|$)", re.I)


def bound(text):
    """(sense, value, unit, label) of '≤ 3 min'; yes/no -> ('bool', True/False/None, '', ''); else None."""
    t = text.strip().rstrip(".")
    low = t.lower()
    if low in ("yes", "required"):
        return "bool", True, "", ""
    if low.startswith("no") and (len(low) == 2 or not low[2].isalpha()):
        return "bool", None, "", ""            # 'no (not required)': no threshold
    m = BOUND.match(t)
    if not m or m.group(4).strip():
        return None
    unit, label = dd.norm_unit(m.group(3))
    n = m.group(2)
    return CMP.get(m.group(1) or "", None), float(n.replace(",", "") if THOUSANDS.fullmatch(n) else n.replace(",", ".")), unit, label


def parse(text):
    groups, cur_group, cur = [], None, None
    for line in text.splitlines():
        s = line.strip()
        hid = dd.HEADING_ID.match(s)
        if s.startswith("## ") and not hid:
            cur_group, cur = {"group": s[3:].strip(), "requirements": []}, None
            groups.append(cur_group)
            continue
        if hid:
            if cur_group is None:
                cur_group = {"group": "Requirements", "requirements": []}
                groups.append(cur_group)
            title = re.sub(r"^[\s—–:.-]+", "", s[hid.end():]).strip()
            cur = {"id": hid.group(1), "kind": "given", "title": title, "statement": "", "metric": "",
                   "sense": "argued", "unit": "", "target": None, "threshold": None, "priority": 1,
                   "raw_target": "", "raw_threshold": "", "checked_by": "", "conditions": {},
                   "interpretation": "", "consequence": ""}
            cur_group["requirements"].append(cur)
            continue
        if cur is None or not s or s.startswith("#"):
            continue
        kv = KV.match(s)
        if not kv:
            cur["statement"] = (cur["statement"] + " " + s).strip()
            continue
        key, val = kv.group(1).strip().lower(), kv.group(2).strip()
        if key in ("target", "threshold"):
            for k, v in PARTS.findall(f"{key}: {val}"):
                cur["raw_" + k.lower()] = v.strip()
        elif key in ("priority", "weight", "importance"):
            m = re.match(r"\d+", val)
            cur["priority"] = int(m.group()) if m else 1
        elif key == "metric":
            cur["metric"] = val
        elif key in ("checked by", "verification", "verified by", "verification method"):
            cur["checked_by"] = val
        else:
            cur["statement"] = (cur["statement"] + f" {kv.group(1)}: {val}").strip()
    for g in groups:
        for r in g["requirements"]:
            bt = bound(r["raw_target"]) if r["raw_target"] else None
            bh = bound(r["raw_threshold"]) if r["raw_threshold"] else None
            nums = [b for b in (bt, bh) if b and b[0] in ("<=", ">=")]
            if nums and len({b[0] for b in nums}) == 1 and all(b is None or b in nums for b in (bt, bh)):
                sense, unit, label = nums[0][0], nums[0][2], nums[0][3]
                r.update(sense=sense, unit=unit, unit_label=label)
                for k, b in (("target", bt), ("threshold", bh)):
                    if b in nums:
                        v = dd.Q_(b[1], b[2]).to(unit).magnitude if unit and b[2] and b[2] != unit else b[1]
                        r[k] = float(f"{v:.6g}")
            elif bt == ("bool", True, "", "") and (not r["raw_threshold"] or bh == bt):
                r.update(sense="bool", target=True, threshold=True if bh else None)
    return groups


def main():
    force = "--force" in sys.argv
    src = dd.RUN / "INPUT" / "requirements.md"
    groups = parse(src.read_text(encoding="utf-8"))
    out_dir = dd.ROOT / "requirements"
    out_dir.mkdir(exist_ok=True)
    n = 0
    for i, g in enumerate([g for g in groups if g["requirements"]], start=1):
        slug = re.sub(r"[^a-z0-9]+", "-", g["group"].lower()).strip("-")[:30] or "group"
        path = out_dir / f"{i * 10:03d}-{slug}.yaml"
        n += len(g["requirements"])
        if path.exists() and not force:
            print(f"keep  {path.relative_to(dd.ROOT)} (exists)")
            continue
        path.write_text(yaml.safe_dump(g, sort_keys=False, allow_unicode=True, width=110), encoding="utf-8")
        print(f"wrote {path.relative_to(dd.ROOT)}:")
        for r in g["requirements"]:     # eyeball each parsed bound against INPUT
            print(f"  {r['id']:<16} {r['sense']:<6} target {r['target']} / threshold {r['threshold']} "
                  f"{r.get('unit_label') or r['unit']}   (as written: {r['raw_target'] or '-'} / {r['raw_threshold'] or '-'})")
    ids = dd.input_ids()
    print(f"{n} requirements imported; the package checker sees {len(ids)} ids in INPUT/requirements.md")
    if n != len(ids):
        print("MISMATCH: add the missing ids by hand (every INPUT id needs an entry and a check)")
    return 0 if n == len(ids) else 1


if __name__ == "__main__":
    sys.exit(main())
files/method/repo-template/tools/scaffold.py (107 wierszy)
#!/usr/bin/env python3
"""Write stubs so a ticket starts from a runnable skeleton. Never overwrites a file.

  ddpy design/tools/scaffold.py checks [--only REGEX]   checks/req_<ID>.py for each requirement without one (make
                                                        scaffold); argued ids also get evidence/<ID>.md
  ddpy design/tools/scaffold.py model T                 model/topo_<T>.py: a @stub for each model function the checks
                                                        call that nothing provides yet, plus the key figures
Stubs are honest: a model stub raises Unknown (NOT RUN) until implemented; an evidence stub carries a [[WRITE ...]]
marker that make verify rejects. Refine every stub: quantity names, conditions, relations, hand estimates.
"""
import argparse
import ast
import keyword
import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import ddlib as dd

STOP = {"a", "an", "the", "of", "to", "at", "in", "on", "for", "and", "or", "with", "per", "by", "from", "is", "be"}
RESERVED = {"topology", "info", "features", "has", "entry", "names", "with_"}
NEEDS = re.compile(r"need\(\s*P\s*,\s*[\"']([A-Za-z_]\w*)[\"']")


def slug(text, fallback):
    s = "_".join([w for w in re.findall(r"[a-z][a-z0-9]*", str(text or "").lower()) if w not in STOP][:4])
    return fallback if not s or s in RESERVED or keyword.iskeyword(s) else s


def check_stub(r):
    rid, sense = r["id"], r.get("sense")
    fallback = "q_" + re.sub(r"\W", "_", rid.lower())
    if sense in ("<=", ">=", "range"):
        kw = "".join(f", {k}=R.cond({k!r})" for k in (r.get("conditions") or {}) if k.isidentifier())
        body = (f'    # the quantity the model must compute, in a unit convertible to the requirement\'s; pass each\n'
                f'    # verification condition: need(P, "<name>", <condition>=R.cond("<condition>"))\n'
                f'    return need(P, "{slug(r.get("title") or r.get("metric"), fallback)}"{kw})\n')
    elif sense == "bool":
        body = (f'    # a documented design decision (params/<T>.yaml features), or compute it from the model\n'
                f'    return "{slug(r.get("title"), fallback)}" in P.features\n')
    else:
        body = ('    # decided by documented design decisions? return (target_met, threshold_met), e.g. from P.features\n'
                f'    raise Unknown("not decided yet: what would compute it", evidence="evidence/{rid}.md")\n')
    return (f'"""{rid} {r.get("title", "")}: scaffold stub - refine what is computed and how it is accepted."""\n'
            'import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))\n'
            'from ddlib import Q_, need, Unknown, run_check\n\n\ndef compute(P, R):\n' + body +
            '\n\nif __name__ == "__main__":\n    run_check(__file__, compute)\n')


def checks(only):
    n = 0
    for rid, r in dd.requirements().items():
        if only and not re.search(only, rid):
            continue
        path = dd.ROOT / "checks" / f"req_{rid}.py"
        if not path.exists():
            path.write_text(check_stub(r), encoding="utf-8")
            n += 1
            print(f"wrote checks/req_{rid}.py ({r.get('sense')})")
        ev = dd.ROOT / "evidence" / f"{rid}.md"
        if r.get("sense") in ("argued", None) and not ev.exists():
            ev.parent.mkdir(exist_ok=True)
            title = r.get("title", "") if not re.search(r"\d", r.get("title", "")) else ""
            ev.write_text(f"# {rid} {title}\n\n[[WRITE: the argument: what the requirement asks, the design decisions "
                          f"or estimates that answer it (quote numbers with p(), o(), req()), and what would compute "
                          f"or test it]]\n", encoding="utf-8")
            print(f"wrote evidence/{rid}.md")
    print(f"{n} check stub(s) written")


def model(T):
    used = {}
    for rel in dd.files("checks/req_*.py"):
        for name in NEEDS.findall(dd.read(rel)):
            used.setdefault(name, []).append(Path(rel).stem[4:])
    for k in dd.KEY_FIGURES:
        used.setdefault(k, ["key figure (manifest.json)"])
    path = dd.ROOT / "model" / f"topo_{T}.py"
    text = path.read_text(encoding="utf-8") if path.exists() else (
        f'"""Physics and cost model of topology {T} (design/README.md "Model"). Scaffold stubs (@stub) raise Unknown until\n'
        f'implemented: replace each by its governing relation with a hand estimate in the docstring; add selftest_* tests."""\n'
        'from ddlib import Q_, need, Unknown, stub, ureg\n\nOUTPUTS = []        # key estimates shown in the documents\n')
    defined = {n.name for n in ast.parse(text).body if isinstance(n, ast.FunctionDef)}
    todo = [n for n in sorted(used) if n not in defined and dd.find(T, n)[0] is None]
    if not todo:
        print(f"model/topo_{T}.py: nothing to add")
        return
    if path.exists():
        text = text.rstrip("\n") + "\n\n\nfrom ddlib import Unknown, stub  # noqa: E402  (scaffold stubs below)\n"
    for n in todo:
        text += (f'\n\n@stub\ndef {n}(P, **conditions):\n    """Used by {", ".join(sorted(set(used[n])))}. Relation: (write). '
                 f'Hand estimate: (write)."""\n    raise Unknown("{n} is not modelled yet for topology {T}")\n')
    path.write_text(text, encoding="utf-8")
    print(f"model/topo_{T}.py: {len(todo)} stub(s): {', '.join(todo)}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("what", choices=["checks", "model"])
    ap.add_argument("topology", nargs="?")
    ap.add_argument("--only", help="regex on requirement ids")
    a = ap.parse_args()
    if a.what == "model" and not a.topology:
        sys.exit("scaffold.py model <T>: name the topology")
    checks(a.only) if a.what == "checks" else model(a.topology)
files/method/roles/ticket-worker.md (54 wierszy)
# Role: ticket worker in a MODEL-TDD design repository

You make ONE well-specified change to the design repository `design/` of this run, described by your ticket file.
The repository and its check results are the only channel: the maintainer reads your files, your change note and
the output of the gates, nothing else. Nobody answers questions; decide with `method/POLICY.md`.

## Before you start
1. `date` (note the start; your ticket has a time box).
2. Read `method/HYGIENE.md` (binding), `design/README.md` (conventions), your ticket, and the files it names.
   Tool commands: `INPUT/TOOLCHAIN.md`; Python is `ddpy`. Run commands from the run directory.
3. If you need the quality bar of a deliverable, read the matching section of `.claude/skills/dd-hw-handbook/SKILL.md`.

## Rules
1. **Write set.** Create or change only the paths your ticket lists. Need something outside it (a common
   parameter, another ticket's module)? Do not touch it: put the request in your change note.
2. **Numbers.** Every number you introduce lives in `params/*.yaml` or `requirements/*.yaml` (or `bom/*.yaml`)
   with unit, origin and note. SOURCE = a page or document you actually read, with the date; a failed lookup
   becomes an ASSUMPTION with a range. Model and check code types no design numbers.
3. **Checks compute, never assert.** A check returns a value; the engine assigns the status. Never shape a
   check, condition, target or model so that it passes; never return a constant. An argued requirement that
   documented design decisions settle returns `(target_met, threshold_met)` from `P.features`; nothing settles it
   → `Unknown` with the reason and an evidence file (reported NOT RUN: it claims nothing). Start from the stubs:
   checks of given ids exist; `ddpy design/tools/scaffold.py checks --only '<your new ids>'` and
   `ddpy design/tools/scaffold.py model <T>` write more (README).
4. **Estimate before you compute.** Each model function's docstring carries the governing relation and a hand
   estimate; each module has a `selftest_*` known-answer test. If model and estimate disagree beyond what the
   model explains, find out why before using either.
5. **Leave the repository runnable.** Your ticket's done-when commands must pass: FAIL statuses are design
   results and allowed; ERROR, MISSING, a failing selftest or a lint error in your files is not.
6. **Prose parts and evidence files** (`docs/parts/*.md.j2`, `evidence/*.md`: both Jinja templates) quote numbers
   with `p()`, `o()`, `req()`, `val()`, `c()` — in a part about one topology pass its id, e.g. `p("mass", "B")`.
   No typed number with a unit (lint rejects it), except an external fact or a number of INPUT on a line that
   carries `SOURCE` and its reference, e.g. `SOURCE INPUT/requirements.md` (README "Documents").
7. **Web research** (only when your ticket asks): load the tools with ToolSearch `select:WebSearch,WebFetch`;
   read pages, do not guess them; record URL and date read. Never open stragowski.suby.pl. Read-only: no
   accounts, forms, quotes or contact.
8. **Never**: run `make check|explore|decide|scaffold|docs|concept|full|checkpoint` or `tools/render.py` (the
   maintainer's CI and renders; you run the scoped commands of your ticket); edit `deliverables/` (unless your
   full-tier ticket names a source folder there); run git commands that change the repository; write files named
   REPORT*, SUMMARY*, FINDINGS* or ANALYSIS*; install software; message anyone; read outside the run directory
   except system and toolchain paths (INPUT/TOOLCHAIN.md) and the web.
9. **Time box.** At the box end stop, leave the repository runnable, and report what is done and what is not.

## Finish
Write `design/tickets/T<nn>.note.md` and end with the same text as your final message:
```
T<nn> <title> — DONE | PARTIAL | FAILED
Files: <paths written>
Checks in scope (run_all --only ...): <counts by status before -> after; ids still UNKNOWN/FAIL with reason>
Parameters added or changed: <name = value unit (origin)>, or "see params/<T>.yaml"
Requests to the maintainer: <changes outside the write set, decisions needed by POLICY>
Doubts: <what you are unsure of and why>
```
files/method/templates/ticket.md (17 wierszy)
# T<nn> — <title>

Ticket for a ticket-worker (MODEL-TDD). Read `method/roles/ticket-worker.md`, `method/HYGIENE.md` and
`design/README.md` first. Run directory: <absolute path>. Commands run from there.

- **Goal**: <one sentence: the change and why it matters for the decision or the package>
- **Context to read**: <requirement ids / group files, topology, model modules, research files>
- **Write set** (the only paths you may create or change): <paths and globs>
- **Do**: <numbered steps; name the model functions, parameters, parts or plots expected>
- **Done when**:
  - `ddpy design/checks/run_all.py --only '<id regex>' [--topology <T>]` shows no ERROR or MISSING
  - `ddpy design/tools/lint.py --scope '<id regex or path>'` reports no error
  - <ticket-specific check, e.g. every needed model function exists for topology T: `make -C design needs`>
- **Allowed failures**: FAIL statuses (design results), UNKNOWN with reason and evidence file
- **Time box**: <minutes> (from your first `date`); at the box: stop, leave the repo runnable, report
- **Report**: write `design/tickets/T<nn>.note.md` (format in the role file) and end with the same text
files/method/templates/topology.yaml (13 wierszy)
# params/<T>.yaml — one topology = a working principle + its parameter set. Copy, fill, keep every number here.
topology:
  id: A                      # A, B or C; file name params/A.yaml
  name: ""                   # short name, e.g. the working principle in 3-5 words
  principle: ""              # 1-3 sentences: how it works and why it differs from the others in principle
features: []                 # design decisions that bool/argued checks test, e.g. [physical_controls, hw_thermal_cutoff]
params:
  # design variables (you choose them; give the exploration range):
  # x_name: {value: 0, unit: "", kind: design, origin: ASSUMPTION, note: "first guess: why", range: [0, 0]}
  # facts (materials, components, physics data); an ASSUMPTION fact's range covers the plausible values (make decide
  # tests both ends). A name of params/common.yaml here overrides it: say "override: <reason>" in the note.
  # y_name: {value: 0, unit: "", kind: fact, origin: SOURCE, note: "https://... read YYYY-MM-DD"}
skills/dd-hw-handbook/SKILL.md (266 wierszy)
---
name: dd-hw-handbook
description: Product-agnostic quality bars and methods for every deliverable of a dd hardware design run (requirements, concept, calculations, CAD, drawings, electronics, firmware, safety and certification, BOM and cost, prototype and test plan, renders, report). Read it before producing any deliverable. It says WHAT GOOD LOOKS LIKE; your main prompt says who does the work and in which order.
---

# dd hardware design handbook

Your main prompt defines the team, the roles, the communication and the order of work. This handbook defines
the quality bar of each deliverable and the methods that reach it. Where your main prompt is more specific,
follow your main prompt. Where the RUN CONTRACT fixes a file name or a format, the RUN CONTRACT wins.
Exact tool commands, versions and paths are in `INPUT/TOOLCHAIN.md` (read the working examples it points to
before writing your own scripts); this handbook names tools only by purpose.

Contents: 1 inputs · 2 requirements · 3 research · 4 physics map · 5 concept · 6 calculations · 7 CAD ·
8 drawings · 9 electronics · 10 firmware · 11 safety and certification · 12 sourcing, BOM and cost ·
13 prototype and test plan · 14 renders · 15 report and manifest · 16 claims · 17 time and context.

---

## 1. Reading the inputs

- `INPUT/brief.md` — one sentence plus customer context. Extract: who buys it, where and how it is used, how
  often and how long, the price expectation, the one or two things that must be better than what exists.
- `INPUT/requirements.md` — the acceptance criteria. Each item has an id, a metric, a target and usually a
  threshold (worst acceptable), a verification method and a weight. **Every id must be addressed and keep its
  exact spelling** wherever you mention it. Weights tell you what matters most when requirements conflict.
- `INPUT/TOOLCHAIN.md` — what is installed and how to call it headless. Use only what it lists. A tool that
  misbehaves gets three honest attempts with new information each time, then a labelled fallback (for example
  an analytical model instead of a simulation) and a note in `manifest.json → blocked[]` if it mattered.

## 2. Requirements — `deliverables/01-requirements/`

Quality bar: an engineer who has never seen the brief can design and verify the product from this folder
alone, and every number in it has an origin.

Write `requirements.md` (and, if you like, a machine-readable `requirements.yaml`) with three parts:

1. **Given requirements**, one entry per id from `INPUT/requirements.md`: the id, the original statement, your
   interpretation (the exact condition under which it will be verified: temperature, voltage, distance,
   duty cycle, test method), and the design consequence (what it forces in the design).
2. **Derived requirements**, ids `DR-<AREA>-<nn>`, each with: parent id(s) or "brief"/"regulation"/
   "manufacturing", statement, metric, target, threshold, verification method (Analysis, Inspection,
   Demonstration, Test), rationale with origin label, priority. Walk at least these areas and write down
   either the requirement or why the area does not apply: function and performance under all operating
   conditions; user interface and ergonomics; power source and consumption (including standby); mechanical
   interfaces and mounting; environment (temperature, humidity, dust and water, altitude, transport and
   storage); safety (electric shock, heat and burns, fire, mechanical hazards, chemical, optical and other
   radiation, noise, misuse that can be foreseen); EMC and, if any radio, radio; materials and restricted
   substances; lifetime, duty cycle and reliability; maintenance, cleaning, consumables and repair;
   manufacturing process at 1, 100 and 1000 units; assembly and test in production; packaging, shipping and
   drop; labelling, manuals and markings; unit cost and retail price; prototype schedule.
3. **Conflicts and decisions**: requirement pairs that pull against each other, how you resolved each (use the
   weights; safety first), and the assumptions register (each ASSUMPTION with reason and sensitivity).

Write `compliance.csv` exactly as the RUN CONTRACT specifies, one row per id of `INPUT/requirements.md`:
- `status`: `met`, `partly`, `not_met` or `not_addressed`. At the concept tier the status is a prediction
  from your estimates: say so in `note` (for example "predicted, CALCULATION"). Be honest: a predicted miss
  is `not_met` with the reason, not a hidden problem.
- `value` and `unit`: the predicted or designed value in the requirement's own metric and unit.
- `evidence`: path(s) inside `deliverables/` that show it (a calculation result, a section of the concept).

## 3. Research — benchmark, users, prior art

- Find at least five comparable products (commercial and open-source; adjacent categories count). For each:
  the specifications that matter for our requirements, price with date and URL, and weak points from
  reviews, teardowns, forums or return reasons. Put the table in `02-concept/benchmark.md`.
- From the benchmark derive: realistic target values, the differentiator ("our own, better version" — what we
  do better and why it is feasible), and the failure modes users actually complain about. Feed both back into
  the derived requirements.
- Note patents only as prior art you noticed; never give legal opinions. If an idea of yours looks genuinely
  new, flag it as `novelty` in `REPORT.md` (one line) — do not elaborate on it in public-facing text.
- Read pages, do not guess them. If a site blocks automated reading, use a different source.

## 4. Physics map — which disciplines decide this product

Rate each domain CRITICAL, RELEVANT or MINOR for this product: structure and strength; heat; fluids and
aerosols; electrical power; electronics and control; firmware; mechanisms and motion; materials and
chemistry; optics and light; acoustics; human factors; environment. For each CRITICAL domain write: the
governing relations, the few parameters that dominate the result, the order-of-magnitude numbers, and the
ways it typically fails (from first principles and from what users report). The map decides which analyses
you run and how deep; it lives in `02-concept/physics-map.md`.

## 5. Concept — `deliverables/02-concept/`

Quality bar: the decision is visibly earned. A reader sees real alternatives, numbers for each, a fair
comparison, and a chosen architecture concrete enough to start detailed design tomorrow.

Contents (file names are suggestions; keep them inside `02-concept/`):
- `concept.md` — the main document, in this order:
  1. The product in one paragraph: what it is, for whom, what it does better.
  2. Functions and flows: the main functions as verb–noun pairs and the energy, material and signal flows.
  3. Alternatives: at least three that differ in **working principle or architecture**, not in cosmetics.
     For each: how it works, a simple diagram, first-order numbers against the top-weighted requirements
     (performance, power, size, mass, unit cost), the main risks and hazards.
  4. Evaluation: knock-out by thresholds first; then a weighted comparison whose weights come from the
     requirement weights; a sensitivity check (vary each weight ±20 %, and the most uncertain estimates
     within their range) showing whether the winner holds. Record why each loser lost.
  5. The chosen concept: architecture block diagram (Mermaid in the markdown and a rendered PNG or SVG if
     the toolchain has a renderer), the subsystem list with make-or-buy, the critical components with real
     candidate parts (manufacturer, part number, supplier link, price with date), the interfaces between
     subsystems (mechanical, electrical, thermal, data).
  6. Key estimates: every number the decision rests on, each with its origin; the scripts that compute them.
  7. Safety concept: the top hazards, the design measures (inherently safe design first, then protective
     devices, then information for the user), and which protections act **independently of firmware**.
  8. Compliance route: which EU directives and harmonised standards, and which FCC rules, apply and why
     (with SOURCE), the likely conformity route, rough cost and time.
  9. Cost: a rough BOM of the major items, the unit cost at 1, 100 and 1000 units with the method, and the
     retail price logic (section 12). Optionally `09-bom-cost/cost.json` already at this tier.
  10. Risks: the top risks with likelihood, impact, mitigation and the analysis or test that would retire
      each one.
  11. Decisions taken without a human (taste, priorities, trade-offs) with the rejected alternatives.
  12. Open issues and what detailed design must do first.
- `calc/` — the scripts behind the key estimates (same rules as section 6), with their results files.
- `benchmark.md`, `physics-map.md`, and anything your method produces (scores, votes, issue logs, models).

Concept anti-patterns: two strawmen next to the favourite; alternatives that differ only in colour or
material; scores without numbers behind them; a decision matrix whose weights were tuned until the favourite
won; a concept that silently misses a top-weighted requirement; prose without a single calculated number.

## 6. Calculations — `05-calculations/` (full tier) and `02-concept/calc/` (concept tier)

- One Python script per question, run with the toolchain's Python (`ddpy`, see `INPUT/TOOLCHAIN.md`; the
  system `python3` has no scientific libraries). Physical quantities carry units (the `pint` library) and every
  input is labelled at the top of the script with its origin.
- Scripts run with `ddpy <script>` from their own folder, offline, in under five minutes, and write
  `<stem>.results.json` (`{"<name>": {"value": x, "unit": "..."}}`). The RUN CONTRACT requires this for
  `05-calculations/`; use the same convention in `02-concept/calc/`.
- Order of work: hand estimate → script → (only if it changes a decision) simulation. For any simulation:
  first a known-answer case, then the real case on at least three mesh or step sizes with the trend shown,
  sanity checks (balances of mass, energy or force), and a sensitivity run on the least certain inputs.
- Static strength is not drop, impact or fatigue; steady thermal is not transient; say which remain open.
- Margins: state each result as value, requirement, margin, and status.

## 7. CAD — `03-cad/` (full tier)

- Parametric code-CAD (CadQuery or build123d, whichever `INPUT/TOOLCHAIN.md` lists as working), one script
  per designed part, parameters at the top, an assembly script placing parts with names. Re-running the
  scripts reproduces every STEP. FreeCAD files are acceptable if that is your route.
- Deliver `assembly.step` and one STEP per designed part, plus the source. Purchased parts appear as
  simplified bodies with their real envelope dimensions from the datasheet (label them).
- Check every part: valid solid, volume and mass from density, bounding box against size requirements,
  pairwise interference zero (or an intended fit), clearances for assembly tools and fingers, wall thickness
  and draft suited to the chosen process, standard fasteners.
- Choose a manufacturing process per part for the prototype (3D printing, CNC, laser-cut sheet) and for
  1000 units (injection moulding, die cutting, sheet-metal bending), and note what changes between them.
- Export a GLB of the assembly for the renders and the website (see `INPUT/TOOLCHAIN.md` for the converter).

## 8. Drawings — `04-drawings/` (full tier)

- A drawing for every manufactured part: views, overall and functional dimensions, tolerances (a general
  tolerance class plus explicit tolerances on functional features), material, finish, quantity, part number
  and revision matching the BOM. Flat patterns for sheet metal.
- An assembly drawing with item balloons that match `bom.csv` item numbers. PDF or SVG; DXF optional.
- Open each output after you write it (render a PNG preview and look at it, or parse it) — a drawing file
  that does not open does not exist.

## 9. Electronics — `06-electronics/` (full tier)

- Start from a block diagram and a power budget over the full supply range and all operating modes.
- KiCad 9 project with schematic and, where custom electronics is needed, a routed board. Real parts with
  matching symbols and footprints; every part has a manufacturer part number that exists at a distributor or in
  the offline LCSC/JLC catalogue described in `INPUT/TOOLCHAIN.md`.
- Protection where it applies: input fuse or current limit, reverse polarity, transients and ESD on external
  connectors, inrush, thermal shutdown of hot parts in hardware, safe state on power loss.
- Calculations: currents, trace widths, dissipation and temperature rise of the hot parts, derating, and for
  any mains part the creepage and clearance with the standard you took the values from.
- Run ERC and DRC from the command line; deliver the reports; fix every error or justify each waiver.
  Fabrication outputs: Gerbers, drill, pick-and-place and the board BOM. Passing DRC is not a hardware test.
- Simulate uncertain circuit behaviour with ngspice where it changes a decision (start-up, protection trip,
  filter response) after one known-answer test of the simulator.

## 10. Firmware — `07-firmware/` (full tier)

- PlatformIO project at the folder root that builds offline for the real target MCU of the schematic; use
  exactly the platform versions `INPUT/TOOLCHAIN.md` pins (anything else downloads packages and blocks
  other runs).
- A state machine with explicit fault states and a safe default; a watchdog; inputs debounced; every
  safety-relevant reaction has a hardware backstop and the firmware is never the only protection.
- Host tests (`native` environment) for the logic and the interlocks: write a test that fails first, then
  passes. Build with warnings as errors where the framework allows. Never flash or power anything.
- Pin map agreed with the schematic; configuration constants generated from or checked against the design
  parameters.

## 11. Safety and certification — `08-safety-certification/` (both tiers: summary in 02; full tier: complete)

- Hazard analysis in the ISO 12100 manner: hazard → hazardous situation → harm → risk estimate (severity ×
  probability) → reduction in the order inherently safe design, protective measures, information for use →
  residual risk. Derive safety requirements with ids and trace them to the design.
- Regulatory map with SOURCE for each item. For the EU think through at least: the Low Voltage Directive
  (2014/35/EU) when any voltage is in its range, the EMC Directive (2014/30/EU), the Radio Equipment Directive
  (2014/53/EU) when there is any radio, RoHS (2011/65/EU), WEEE (2012/19/EU), the batteries rules when there
  is a battery, the General Product Safety Regulation (EU) 2023/988, REACH, ecodesign and standby rules.
  For the US: FCC Part 15 (Subpart B for unintentional radiators, Subpart C for intentional ones) and the
  voluntary safety listings retailers expect. Then find the harmonised product-family standards that
  apply to THIS product by research, and quote which clauses drive the design.
- Prefer architectures that make conformity easy: low voltage inside the product through a certified external
  supply when the power level allows it, pre-certified radio modules, protections that do not depend on
  firmware, materials with suitable flammability ratings near heat and live parts.
- State the conformity route (self-declaration, notified body or not), the tests an accredited lab would run,
  a pre-compliance plan, and a rough cost and duration. You identify; you do not certify.

## 12. Sourcing, BOM and cost — `09-bom-cost/`

- Real, purchasable parts only. For each line: manufacturer, part number, supplier, supplier part number, URL,
  unit price at 1, 100 and 1000 (price breaks as shown by the supplier, currency stated), the date you read it
  in `notes`. A price you could not read is an ASSUMPTION with a range, never a guess dressed up as SOURCE.
  Prefer parts in stock at two or more suppliers, active (not end-of-life), and note a second source for
  critical parts. Use the offline catalogue from `INPUT/TOOLCHAIN.md` first, then distributor pages
  read-only.
- `bom.csv` with exactly the header the RUN CONTRACT gives; `cost.json` and `cost.md` as it specifies.
- The unit cost at each quantity is more than the BOM: PCB fabrication and assembly, mechanical parts (at 1
  and 100 units printed or machined; at 1000 units moulded with the tooling cost amortised and stated
  separately), assembly and test labour, packaging, certification and other one-off costs amortised over
  the quantity (state the assumption), freight to the fulfilment point, and a contingency. Show each line.
- Retail price logic: start from benchmark prices and a target gross margin; check that the price leaves room
  for crowdfunding platform and payment fees, shipping to backers, VAT where it applies, returns and warranty.
  An underpriced product that cannot be delivered is the classic crowdfunding failure; say plainly if the
  numbers do not close.

## 13. Prototype and test plan — `10-prototype-test-plan/` (full tier)

- Build plan for prototype 1: how each part is made, the order of assembly, tools, and the checks after each
  step.
- Bring-up for a person later: first power-up with a current-limited or isolated supply, what to measure
  before applying full power, safety precautions.
- One test per requirement (and per safety requirement): method, equipment, conditions, pass criterion, the
  requirement id it closes. Include pre-compliance EMC and the safety tests a lab would run, and durability
  or life tests where lifetime is a requirement.

## 14. Renders — `11-renders/` (full tier)

- Renders come from the real CAD geometry (the assembly GLB) through Blender in background mode: a product
  view, an exploded view, and a view that shows the user interface. Check the scale against a known
  dimension. Label them as renders of a digital design.
- Never use generative image models to depict the product: they invent geometry that does not exist.
- You cannot judge images reliably by looking. Check what can be measured (file exists, resolution, the
  object fills the frame, parts are present by name) instead of trusting a glance.

## 15. Report and manifest

`REPORT.md` (both tiers) in this order: the product in one paragraph; a key-parameter table (value, unit,
origin label); unit cost at 1/100/1000 and the retail price; status per requirement (from `compliance.csv`);
the top risks and open hazards; decisions taken without a human with their alternatives; what is open,
blocked or not run; for the full tier, the file map. Every number in it must match its home.

`manifest.json` exactly as the RUN CONTRACT specifies, including `key_figures` (use `null` for an unknown
figure, never a guess) and `blocked[]`. List every file under `deliverables/`.

Before you finish: re-read `REPORT.md` and `02-concept/concept.md` against the numbers' homes, check that
`compliance.csv` has one row per requirement id, that every evidence path exists, and that `manifest.json`
parses and lists the files that exist.

## 16. Claims

The project designs real products for honest crowdfunding. In every document: no feature that the design
does not implement, no performance number that is not calculated or sourced, no "certified", "safe",
"waterproof", "AI" or similar word unless the evidence for it is in the deliverables. Under-promise.

## 17. Time and context

- The RUN CONTRACT gives the time target and the hard stop. Plan time boxes per phase at the start, check
  `date` at each boundary, and cut scope (depth, not honesty) when behind.
- Long runs lose context. Keep a short state file (decisions, key numbers' homes, open issues, next step)
  current, so you or a fresh agent can resume from files alone.
- Write deliverables as soon as each part exists and keep them current.