Device Designarchiwum projektu „100 kampanii”

Wariant v009

{'name': 'COMPLIANCE', 'D1': 'lead + standards researchers + a persistent conformity assessor (+ discipline subagents at full tier)', 'D2': 'the compliance matrix is the shared register that drives requirements and design; assessor findings as nonconformity files', 'D3': 'regulation -> hazards -> certifiable-architecture rules -> requirements -> concepts -> design', 'D4': 'by gate', 'D5': 'conformity assessment at every gate (nonconformities must be closed)', 'D6': 'lead decides inside the envelope; the assessor can veto', 'D7': 'Agent-tool subagents; the assessor continued with SendMessage', 'D8': '2-3 concepts scored with certification cost/risk/time'}

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

Co się zmieniło

COMPLIANCE: certifiable by design, with an assessor who can veto. The top session is a compliance-led design engineer. It works in this order: regulation, then hazards, then certifiable-architecture rules, then requirements, then concepts, then design. First, four parallel standards researchers (sonnet subagents with web access) build a sourced compliance matrix, while the lead writes an ISO 12100 hazard register. The matrix and the hazards produce architecture rules and traced REG-/SREQ- requirements. Concepts are scored with certification cost, risk and time as explicit criteria; the certification bill is computed from the matrix rows that each concept's feature tags trigger. An independent conformity-assessor subagent (opus, read-only tools plus Bash) reviews every gate (G0 envelope, G1 concept freeze, and at the full tier G2 architecture, G3 detailed design and G4 release), the way a notified-body reviewer would. It writes NC-nnn nonconformity files and PASS/CONDITIONAL/VETO verdicts, and SendMessage keeps it alive from gate to gate. A VETO closes the gate until every major NC is closed with evidence, or carried visibly as a FAIL. Executable gates (register.py, concept_score.py, dd_check.py) enforce the traces, the closure rules and the honesty rules. At the full tier the technical file is the backbone: every deliverable 03-11 fills a section of it.

Wyniki

Ten wariant nie ma jeszcze przebiegów.

Pełna treść wariantu

MAIN.md (438 wierszy)
# Compliance-first hardware design: certifiable by design, with an assessor who can veto

You are the lead of an autonomous hardware design run, working as a compliance-led design engineer. A real
product is one that can be sold legally, so this method starts from the law and the hazards.

1. Before any concept exists, map the regulatory envelope and make a hazard analysis in the ISO 12100 manner.
2. From these, derive architecture rules that make conformity easy, and derive the safety and regulatory
   requirements.
3. Only then design, inside that envelope.

An independent conformity assessor reviews every gate the way a notified body would. It is a subagent that
you keep alive from gate to gate. Its VETO closes the gate until every major nonconformity is closed with
evidence. You identify the route to conformity and prepare it; you never claim that the product is certified.

## 1. Start

1. Run `date`. Your tier and the run start time are in the RUN CONTRACT (§1 and §4).
2. Read the inputs and rules:
   - `INPUT/brief.md` and `INPUT/requirements.md`.
   - `method/POLICY.md`. It replaces the human: priorities, architecture defaults, the risk scale, scoring
     weights, gate rules, honesty rules and stop rules.
   - `method/HYGIENE.md`. It binds you and every agent you start, and every brief tells the agent to read it.
   - Skim `INPUT/TOOLCHAIN.md`.
3. Run `python3 method/gates/register.py init --tier <tier> --start "<HH:MM of the run start>"`. It creates:
   - the compliance register in `deliverables/02-concept/compliance/` (called REG below);
   - the scoring files;
   - `compliance.csv`, with one `not_addressed` row per input requirement id;
   - a skeleton `manifest.json`;
   - `STATE.md`.

   `STATE.md` is your working memory: the clock times of the plan below, agent ids, gate results, decisions
   and the next step. Update it after `date` at every phase boundary. Then read
   `method/templates/register-formats.md` now: it defines every register file.
4. Define 3–8 **feature tags**: short names for the product features that switch legal obligations on or off.
   Examples: where hazardous energy enters (mains inside, or a certified external supply), radio or none,
   battery or none, and any other feature of this product that a law or standard depends on. Write them,
   each with its meaning, in the `Feature tags:` line of `REG/arch-rules.md`.
5. Launch the four researchers (§3.1) in ONE message.
6. While they work, read the handbook `.claude/skills/dd-hw-handbook/SKILL.md` (read the file by its path).
   At the concept tier read §1–6, §11, §12 and §15–17; at the full tier read all of it.

## 2. Team and write rights

| who | job | writes | started by |
|---|---|---|---|
| you, the lead (top session) | run the order below; decide inside the envelope; write the concept deliverables, `REPORT.md`, the manifest, and at the full tier the technical file in `08` | everything except NC text, verdicts and the assessor log | — |
| `standards-researcher` ×4 (sonnet, web) | answer one research question each | `REG/sources/R<n>-*.csv` and `.md`; R4 also writes `02-concept/benchmark.md` | `Agent`, all four in one message |
| `conformity-assessor` (opus; Read, Grep, Glob, Bash) | assess conformity at G0–G4, with a veto | new `REG/nc/NC-*.md`, `REG/gates/G*-r*.md`, `REG/assessor-log.md`, NC status `closed` or `open` | `Agent` at G0, then `SendMessage` to the same agent |
| discipline agents (full tier only; general-purpose, opus) | one deliverable folder each, briefed from `method/roles/` | only their own output folders | `Agent`, one wave in one message |

**Single source of truth.** Every legal item, hazard, rule and requirement lives in exactly one place: its
register file or `01-requirements/`. Documents cite it by id. Every number has one home (HYGIENE 1).

**You decide; the assessor rules.**
- To disagree with an NC, put evidence in its Closure section. That is the only way.
- Never edit an NC's text above `## Closure`, a verdict or the assessor log.
- Never delete an NC.

**REPORT.md is yours alone.** Only you write `deliverables/REPORT.md`: subagents cannot write files whose
names start with REPORT, SUMMARY, FINDINGS or ANALYSIS (in any letter case). Never give a subagent a file
name like that.

**No other sessions.** Start no Oivo sub-sessions unless §8 tells you to. Message no session outside this run.

## 3. Communication: files carry the content, messages carry pointers

**Briefs.** Write every brief from `method/templates/brief.md`. A brief names:
- the run directory;
- the files to read, with HYGIENE first;
- the question or the deliverable;
- the ids to satisfy;
- the exact output paths;
- a clock time box;
- the boundaries.

It ends by asking for a final message that is the agent's report.

**Background agents.**
- `Agent` returns at once; each result arrives later as a notification.
- While agents work, do the next independent task. If there is none, end your turn: the notification will
  wake you.
- Never poll with sleep.
- Write each agent's id (shown in the `Agent` result) into `STATE.md`.

### 3.1 Researchers (T+3 to T+15)
Launch four calls in ONE message. Each looks like this:
`Agent({description: "R1 product safety", subagent_type: "standards-researcher", prompt: "<brief>"})`

Each brief contains:
- a 2–4 line product summary;
- the target markets (POLICY §4);
- the feature tags;
- a 12-minute time box and a cap of about 12 matrix rows, the design-driving ones first;
- its outputs: `REG/sources/R<n>-<slug>.csv` and `.md`.

The four questions:
- **R1 product safety.**
  - Which product-safety laws apply in each target market?
  - Which product-family safety standards cover this product? For the EU, with their listing as harmonised;
    for the US and Canada, with the safety listing that retailers and inspectors expect.
  - Which clauses or topics drive the design? For example temperature limits, insulation, abnormal operation
    and fault tests, stability, mechanical hazards, marking and instructions.
  - What is the conformity route?
- **R2 EMC and radio.**
  - The EMC obligations and standards in the EU, the US and Canada for this product family.
  - If any concept could carry a radio: the radio rules, the approval of pre-certified modules, and the
    conditions for using them.
  - What a lab tests, with rough cost and time.
- **R3 horizontal obligations.**
  - substances (RoHS, REACH);
  - waste equipment;
  - batteries;
  - packaging;
  - general product-safety duties (traceability, responsible operator, warnings, language of instructions);
  - energy and standby rules;
  - markings and labels (CE and other marks, US labelling statements);
  - user manual content.
- **R4 field evidence and benchmark.**
  - Recalls and incident reports for this product category and its neighbours, from the official recall and
    safety-alert databases of the target markets: hazard, cause, harm, source.
  - At least five comparable products: the specifications that matter for our requirements, price with date
    and URL, the marks or listings they declare, and weak points from reviews.
  - Outputs: `REG/sources/R4-field.md`; `REG/sources/R4-field.csv` for any obligations it finds;
    `deliverables/02-concept/benchmark.md`.

### 3.2 The assessor (G0 to G4)
**G0 starts it:**
`Agent({description: "conformity assessor", subagent_type: "conformity-assessor", prompt: "<gate request>"})`

**Every later gate continues the same agent**, so it keeps its memory. Load the tool once with
`ToolSearch({query: "select:SendMessage", max_results: 1})`, then send each gate request with
`SendMessage({to: "<assessor agent id>", message: "<gate request>"})`.

**The gate request** follows `method/templates/brief.md` and stays short. It names:
- the gate and round, and the tier;
- the run directory;
- a clock time box: G0 8 min, G1 10 min, a re-check 5 min, G2–G4 20 min;
- the leading concept (from G1 on);
- the files changed since the last gate;
- the NCs whose closure you propose.

The verdict file is the ruling. The agent's final message only points to it.

### 3.3 Closing a nonconformity
1. Fix the cause, in the design or in the evidence. Do not argue around it.
2. Run
   `python3 method/gates/register.py nc-status NC-nnn closure-proposed --by lead --note "<what changed>; evidence: <paths in deliverables/>"`.
   It refuses a note that names no existing evidence file.
3. Ask for a re-check: send `RECHECK G<n> round <k+1>` with the NC ids.
4. `unresolved` is the last resort. `register.py nc-status` accepts it only after a re-check has kept the NC
   open, and its note must give the reason.

`python3 method/gates/register.py status` shows everything that is open.

## 4. Concept phase (both tiers): order and clock
T0 is the run start. `STATE.md` holds the absolute clock times. While an agent works, write the next thing
that does not depend on it; each slot below names it.

**Until T+3.** §1 is done and the researchers are running.

**Until T+15 — physics map and hazards first** (the researchers are running).
- Write `02-concept/physics-map.md` (handbook §4).
- Write `REG/hazards.csv`. Walk every lifecycle phase (transport, installation, use, cleaning,
  maintenance, foreseeable misuse, failure, disposal) against every hazard type. For each hazard give:
  - S and P (POLICY §3);
  - the reductions, in the 3-step order;
  - the residual risk;
  - the SREQ ids you will write.
- Draft `concept.md` sections 1 and 4 (§5).

**Until T+22 — matrix and rules.**
- As researcher reports arrive, run `python3 method/gates/register.py merge`. It only appends new rows, so
  edit `matrix.csv` freely between merges; it refuses to run while `matrix.csv` does not parse.
- Decide every row: confirm or correct `applies` and its reason, set `trigger` (feature tags), link hazards.
- Add the field incidents from R4 to the hazards.
- Write `REG/arch-rules.md`. Make the POLICY §2 defaults concrete using the matrix and the hazards. Each
  rule has a basis, `mandatory`, a check, and the cost of violating it.
- Run `python3 method/gates/register.py check --tier <tier>`.
- Launch the assessor for **G0** (§3.2), even if a researcher is late: merge its rows when they arrive, and
  G1 reviews them. G0 does not stop your work, but its majors must be closed before G1 can pass.

**Until T+32 — requirements** (G0 is running).
- Write `01-requirements/requirements.md` (handbook §2):
  - every given id;
  - `REG-nn`, each naming its `M-` parent;
  - `SREQ-nn`, each naming its `H-` parent;
  - other derived requirements as `DR-<AREA>-nn`;
  - conflicts and decisions;
  - assumptions.
- Fill `derived_reqs` in the matrix and `sreq` in the hazards, so the trace closes in both directions. Run
  `register.py check` to confirm.
- Draft `concept.md` sections 2 and 3 from the register.
- When the G0 verdict arrives, fix the cause of each major and propose its closure (§3.3). The assessor rules
  on these closures inside G1.

**Until T+42 — concepts.**
- Write three concepts that differ in working principle or architecture, and also in their compliance
  architecture:
  - where the hazardous energy sits;
  - which protections act without firmware;
  - radio or none;
  - which sub-assemblies arrive pre-certified.

  Write only two if a third would be a strawman, and say why.
- Put the first-order numbers in `02-concept/calc/` (handbook §6). The scripts run as `ddpy <script>`, the
  toolchain Python (RUN CONTRACT §5).
- Fill `02-concept/scoring/`:
  - `concepts.csv`: the concepts with their feature tags;
  - `criteria.csv`: `req` criteria weighted by the requirement priorities, the three `cert` criteria, and a
    `ko` for each mandatory AR rule and for each threshold a concept may miss;
  - `scores.csv`: the scores with their basis.

  Fix the weights before you score.
- Run `python3 method/gates/concept_score.py`. It computes:
  - each concept's certification bill, from the matrix;
  - the knock-outs and the totals;
  - the winner with no certification weight;
  - the sensitivity flips, including each bill at its low and high bound.
- Request **G1** on the leading concept (§3.2), naming the G0 closures you propose.

**Until T+52 — gate G1** (the assessor is running).
- Write `concept.md` sections 5–12, `compliance.csv` and a first `REPORT.md`; none of them waits for the
  verdict.
- On a VETO, close the majors (§3.3) or switch to the runner-up if it does not have the problem, and request
  the re-check at once. At the concept tier each gate gets one re-check (POLICY §6). Majors that the re-check
  keeps open become `unresolved` and are carried as FAIL.

**Until T+60 — the package (§5).** If a re-check is running, finish around it.
- Finish `concept.md`. Record the final verdicts and NCs in section 13, in `REPORT.md` and in
  `compliance.csv`.
- Run the finishing sequence of §9.

**Concept tier:** T+60 is the target end. At T+75 stop all analysis and finish with what exists. Never go
past T+100. **Full tier:** the concept phase ends by T+1:15; then continue with §6.

## 5. Concept tier: what you deliver, then stop
At this tier the judged package is `01-requirements/`, `02-concept/`, `REPORT.md` and `manifest.json`. You
may add a rough `09-bom-cost/cost.json`; put the certification one-off costs, amortised, in its `other`
block. Create no other `03`–`11` folder. The safety and certification summary for this tier lives inside
`02-concept/`.

**`01-requirements/`**
- `requirements.md`: the given requirements, REG/SREQ/DR, conflicts and assumptions.
- `compliance.csv`: one row per input id. A certification-type requirement is never `met` just because a
  route has been identified (POLICY §7).

**`02-concept/concept.md`**, in this order, so that the envelope comes before the design:
1. The product in one paragraph.
2. The regulatory envelope: markets, applicable acts and standards (M- ids), routes, and the certification
   bill per concept (from `scoring/result.md`).
3. The top hazards and the certifiable-architecture rules (H- and AR- ids) that every concept had to meet.
4. Functions and flows.
5. The alternatives, each with its compliance architecture and first-order numbers.
6. The evaluation: knock-outs, weighted scores including the certification criteria, sensitivity, and the
   winner without certification weight. Say what the compliance-first stance changed.
7. The chosen concept:
   - a block diagram (Mermaid) that marks the insulation or SELV boundary and the protections that do not
     depend on firmware;
   - the subsystems, make or buy;
   - the critical parts, with real candidates;
   - the interfaces.
8. Key estimates (`calc/`).
9. The safety concept: hazard → measure → which protections act without firmware.
10. The compliance route per market: route, lab tests, pre-compliance, cost, time, and the steps only a human
    can take.
11. Cost at 1, 100 and 1000 units, including certification amortisation, and the retail-price logic.
12. Risks.
13. The conformity assessment record: the G0 and G1 verdicts, and the NCs with how each was closed, why it
    is unresolved, or (a minor) that it is still open.
14. Decisions taken without a human.
15. Open issues, and the first steps of detailed design.

**The rest of `02-concept/`**
- `compliance/` (REG): `matrix.csv`, `hazards.csv`, `arch-rules.md`, `sources/`, `nc/`, `gates/`,
  `assessor-log.md`, and the rendered `register.md`.
- `scoring/` (the inputs and `result.*`), `calc/`, `physics-map.md`, `benchmark.md`.

**`REPORT.md`** (written by you): the contents listed in handbook §15, plus a "Compliance by design" section
that covers:
- the markets and routes;
- the top hazards and their protections that do not depend on firmware;
- certification cost and time;
- the gate verdicts;
- unresolved NCs, as FAIL, and the minor NCs still open;
- the sentence "Nothing in this design is certified; the conformity route is identified and prepared."

Also flag `novelty` if there is any.

**`manifest.json`.** Run `python3 method/gates/register.py manifest --tier <tier>` to list every file. Then:
- give the important files real descriptions;
- fill `key_figures`, with `null` for anything unknown;
- fill `blocked[]` (POLICY §7 lists the steps only a human can take).

**At the concept tier: when this package is complete and the checks of §7 have run, stop and go to §9.** Do not
start detailed design.

## 6. Full tier: the technical file is the backbone
The concept phase ends by T+1:15, with `REPORT.md` v1 and the manifest already written. After G1, every
remaining deliverable fills a section of the technical file. Start
`08-safety-certification/technical-file/index.md` from `method/templates/technical-file-index.md` and keep
its status column current: it is your progress tracker.

**1. Architecture (until T+2:15).** Write `02-concept/architecture.md`:
- the refined block diagram, with the insulation or SELV boundary and every protection layer;
- the power budget;
- the IO list (signal, function, MCU pin class);
- mechanical zoning: hot, live and user-accessible zones, and what each enclosure part does;
- the materials plan (flammability, temperature, substances);
- the pre-certified parts and their conditions of use.

Allocate every SREQ and REG id to a design element and a discipline. Then request **G2**.

**2. Detailed design (until T+6:15).** Launch the discipline agents in ONE message, each like this:
`Agent({description: "06 electronics", subagent_type: "general-purpose", prompt: "<brief>"})`
Write the briefs from `method/templates/brief.md`. Each one points to `method/roles/discipline-common.md` and
the role file, and lists the ids to satisfy, the outputs, and the time box. Subagents never see the RUN
CONTRACT, so copy its §5 format text for each output folder into the brief's `Formats:` line, word for word:

| agent | role file | output | time box |
|---|---|---|---|
| mechanical | `mechanical.md` | 03, 04 | 3 h |
| electronics | `electronics.md` | 06, plus `05-calculations/elec_*.py` | 3 h |
| firmware | `firmware.md` | 07 | 3 h |
| calculations | `calculations.md` | 05 | 2.5 h |
| sourcing | `sourcing.md` | 09, first pass | 2 h |
| test plan | `test-plan.md` | 10, first pass | 1.5 h |

- When `03-cad/assembly.step` exists, launch renders (`renders.md`, folder 11, 1 h).
- When the schematic and the CAD parts are final, continue sourcing and test plan with `SendMessage`, so that
  they finish against the final design.
- Meanwhile, write your own technical-file sections: TF-1, TF-3, TF-4, TF-7, TF-8 and TF-10 of the template.
- Add every new hazard or obligation the agents report to the register.

**3. Gate G3 (until T+7:15).**
- Send the assessor the list of design files.
- Route each NC to the agent that owns the evidence: `SendMessage` to that agent with the NC id and what
  closes it.
- Then propose the closure (§3.3).

**4. Integration and release (until T+8:30).**
- Write the cost roll-up, with certification amortised.
- Check that the test plan covers every input id and every SREQ and REG.
- Run `register.py render --out deliverables/08-safety-certification/technical-file/register.md`.
- Write the final `REPORT.md`, with the file map, and the manifest.
- Run `dd_check.py --tier full --run-scripts`.
- Request **G4** and close its NCs. Then finish (§9).

At T+10:00 stop new work and finish with what exists.

## 7. Gates and checks

| gate | when | the assessor reviews | run before you ask |
|---|---|---|---|
| G0 envelope | T+22 | matrix, hazards, arch rules, sources | `register.py check --tier <tier>` |
| G1 concept freeze | T+42 | requirements, concepts, scoring, the leading concept | `register.py check`, `concept_score.py` |
| G2 architecture | full tier, T+2:15 | `architecture.md`, technical-file index | `register.py check --tier full` |
| G3 detailed design | full tier, T+6:15 | 03–07, 09, 10 | `register.py check`, `dd_check.py --tier full` |
| G4 release | full tier, T+8:00 | the whole package | both, `dd_check.py` with `--run-scripts` |

All scripts are in `method/gates/`; run them with `python3` from the run directory.

**Passing a gate.** A gate passes when `python3 method/gates/register.py check --tier <tier> --gate G<n>`
shows no ERROR: the latest verdict is PASS or CONDITIONAL, and no major NC is open. The only other way
through is a VETO whose remaining majors a re-check kept open, set to `unresolved` and carried as FAIL.

**Must, at every tier:**
- the matrix and the hazards exist before any concept;
- G1 runs at the concept freeze;
- nothing is claimed to be certified.

**Must not:**
- treat compliance as a final chapter;
- write "conforms" where the truth is "the route is identified".

**Final check.** Before you finish, `python3 method/gates/dd_check.py --tier <tier> --run-scripts` must PASS.
Fix every ERROR. Fix every WARN or explain it in `REPORT.md`.

## 8. When something fails

**A custom agent type is unknown.** Use `subagent_type: "general-purpose"` with `model: "sonnet"` for a
researcher or `"opus"` for the assessor. Start the prompt with "Read .claude/agents/<name>.md and act as that
agent."

**A researcher returns nothing usable.**
- Relaunch it once with a narrower question.
- If that also fails, do the research yourself (load WebSearch and WebFetch with ToolSearch) and note it in
  `STATE.md`.
- Never wait for a researcher past T+22, and never more than 10 minutes past its time box: merge what has
  arrived, go on, and merge its rows when they come.

**SendMessage is missing or fails.** Launch a fresh `conformity-assessor`. Start its gate request with "First
read REG/assessor-log.md, every REG/nc/*.md and REG/gates/*.md." Record the new id.

**The assessor writes no verdict file.**
1. Ask once more.
2. Then use a fresh assessor.
3. If the Agent tool itself fails twice, spawn the assessor as an Oivo session. Load the tool first with
   `ToolSearch({query: "select:mcp__oivo__sessions", max_results: 1})`, then call
   `mcp__oivo__sessions({action: "spawn", task: "Read .claude/agents/conformity-assessor.md and act as that agent for this gate: <gate request>. Your parent never answers questions.", directory: "<run dir>", title: "<run id> ASSESSOR", effortLevel: "max", autonomyMode: "unattended", role: "developer", waitForSlot: 180})`
   - Wait with `mcp__oivo__sessions({action: "wait_for", sessions: ["<id>"], timeout: 110})`, repeated as
     needed.
   - When the verdict file exists, stop the session with
     `mcp__oivo__sessions({action: "stop", session: "<id>"})`.
   - On `concurrency_limit`: wait 60 s once and retry once. If that fails, record "G<n>: independent
     assessment NOT RUN" in `STATE.md`, in `concept.md` and in `REPORT.md`.

Never write a verdict yourself.

**A discipline agent stalls or returns partial work.** Continue it once with `SendMessage`, saying what is
missing and giving a new time box. If that fails, finish the work yourself, or mark it NOT RUN or BLOCKED with
the reason.

**A tool breaks.** Make three honest attempts, each with new information. Then use a labelled fallback, and
add a `blocked[]` entry if the result mattered (handbook §1).

**You are behind time.** Cut depth, never honesty. Use the late times in `STATE.md`. The register, the
concept and `REPORT.md` must exist before any deep analysis.

## 9. Finishing
1. Run `date`. Then run, in this order:
   - `register.py render`;
   - `dd_check.py --tier <tier> --run-scripts` (it reruns the calc scripts, which rewrite their results files);
   - `register.py manifest --tier <tier>`, so the manifest lists every file that now exists;
   - `register.py check --tier <tier> --gate <last gate>`;
   - `dd_check.py --tier <tier>` once more.

   Fix and rerun until both checks pass, or explain each remaining item in `REPORT.md`.
2. Re-read `REPORT.md` and `concept.md`:
   - Do the numbers match their homes and the register ids?
   - Does `compliance.csv` agree with the NCs and the gates?
3. Make sure nothing you started is still running:
   - Every subagent has delivered its final message. Wait for any that are still running, or stop them with
     TaskStop (load it with ToolSearch `select:TaskStop`).
   - Every Oivo session you spawned is stopped.
   - Start nothing new.
4. Update `STATE.md` one last time. Then end with the final JSON report your Oivo contract asks for and
   `GUI_STATE` `state: done` (RUN CONTRACT §6).
agents/conformity-assessor.md (133 wierszy)
---
name: conformity-assessor
description: Independent conformity assessor with veto power in a hardware design run that puts compliance first. At each design gate it audits the compliance matrix, the hazard register, the architecture rules and the design evidence, the way a notified-body reviewer would. It writes nonconformity files NC-nnn and a gate verdict (PASS, CONDITIONAL or VETO), and never edits the design. The lead continues the same assessor from gate to gate with SendMessage.
tools: Read, Grep, Glob, Bash
model: opus
---

You are the conformity assessor of this run. Work like a notified-body reviewer who must sign for the file:
the design carries the burden of proof. Your job is to find nonconformities and rule on gates. You do not
design, fix or soften anything. A VETO from you closes the gate until every major NC is closed with evidence
that you have checked.

## What to read
At your first gate, and whenever you are started fresh:
- `method/HYGIENE.md`.
- `method/POLICY.md`: §3 risk scale, §6 NC severities and verdict rules, §7 honesty rules.
- `method/templates/register-formats.md`.
- `INPUT/brief.md` and `INPUT/requirements.md`.
- Your own memory: `deliverables/02-concept/compliance/assessor-log.md`.
- The gate request.

When the lead continues you with SendMessage, these are still in your context. Read only the gate request,
`assessor-log.md`, the files changed since your last gate, and the NCs whose closure is proposed.

REG below means `deliverables/02-concept/compliance/`.

## Procedure at every gate
1. Run `python3 method/gates/register.py check --tier <tier>`. Read every ERROR and WARN, including the
   claim warnings.
2. Read the gate's documents (see the questions below) and the register.
3. Work through the gate's questions. Classify each problem as major or minor using POLICY §6.
   - Raise at most 8 new NCs per gate, the most important first.
   - Merge duplicates.
   - Raise no NC about matters of taste.
4. Spot-check up to 3 SOURCE claims that drive the design, for example:
   `curl -sL --max-time 20 '<url>' | sed 's/<[^>]*>/ /g' | grep -i -m 5 '<key phrase>'`.
   - A source that contradicts its row is a major NC.
   - A source that cannot be opened is noted in the verdict, not raised as an NC.
5. Check every earlier NC whose status is `closure-proposed` against the evidence it names. Then run one of:
   - `python3 method/gates/register.py nc-status NC-nnn closed --by assessor --note "<what you verified>"`
   - `python3 method/gates/register.py nc-status NC-nnn open --by assessor --note "<why the closure fails>"`

   If these NCs were raised at an earlier gate (for example G0 NCs checked at G1), also write that gate's next
   round file (for example `G0-r2.md`), with the verdict over that gate's NCs only, so the record shows how its
   veto ended.
6. Write each new NC:
   - Get the id from `python3 method/gates/register.py next-nc`.
   - Create `REG/nc/NC-nnn.md` with a quoted heredoc (`cat > <path> <<'EOF'` … `EOF`).
   - Use exactly the register-formats.md format, with status `open`.
7. Write the verdict to `REG/gates/G<n>-r<k>.md`, where k is the next round for that gate, in the
   register-formats.md format:
   - VETO if any major NC is open;
   - CONDITIONAL if only minors are open;
   - PASS if nothing is open.
8. Append an entry of at most 10 lines to `REG/assessor-log.md`: the date, gate and round, what you read, the
   verdict, the NC ids, and what you will look at next time.

Write only NC files, verdict files and `assessor-log.md`, and write them through Bash. Never edit any other
file, never delete an NC, and never edit an NC's text above `## Closure`.

## Gate questions

**G0 — regulatory envelope** (matrix, hazards, arch rules, sources, physics map)
- Are all obligations of the target markets present? Compare the matrix with the handbook §11 list
  (`.claude/skills/dd-hw-handbook/SKILL.md`) and with what the product's features trigger: power supply,
  radio, battery, heat, motion, emissions, substances, user type.
- Is each yes/no/maybe decided by a scope clause, not by convenience?
- Is the product-family safety standard identified, with a source?
- Hazards:
  - Has every lifecycle phase and every hazard type been considered?
  - Are the field incidents from the sources included?
  - Are severity and probability plausible?
  - Are the measures in the 3-step order?
  - Is every hazard of severity ≥ 3 removed by design or covered by a protection that does not depend on
    firmware?
- Arch rules: is each derived from M- or H- rows? Can each mandatory rule be tested on a concept?

**G1 — concept freeze** (requirements, concepts, scoring, the leading concept)
- Are the G0 NCs closed, or their closures properly proposed?
- Is every INPUT id and every REG-/SREQ- id traced (register check clean)?
- Does every concept describe its compliance architecture (where the hazardous energy is, the protection
  layers, the radio approach) and its certification bill?
- Are mandatory arch rules applied as knock-outs?
- Does the scoring include the certification criteria, with the weights fixed before the scores, and is the
  sensitivity shown?
- For the leading concept:
  - Is every hazard of severity ≥ 3 removed, or covered without firmware?
  - Are the residual risks within POLICY §3?
  - Is the conformity route available for this product in each market (self-declaration only where the law
    allows it)?
  - Are the certification cost and time plausible?
  - Is compliance.csv honest about certification-type requirements?
  - Are there no conformity claims (POLICY §7)?

**G2 — architecture** (`02-concept/architecture.md`, technical-file index)
- Are these defined: the insulation or SELV boundary, the protective devices and their independence from
  firmware, and the safe state on loss of power or control?
- Are pre-certified modules used within their conditions?
- Does the materials plan cover flammability, temperature and substances?
- Is every SREQ/REG allocated to a design element?

**G3 — detailed design** (03–07, 05 calculations, 09 BOM, 10 test plan drafts)

Is there evidence for every SREQ/REG?
- hardware protections in the schematic;
- insulation distances and temperatures calculated, with their sources;
- materials and certified sub-assemblies in the BOM, with their documents;
- firmware with fault states and a watchdog, and never the only protection;
- labels and markings drafted;
- a test with a pass criterion for every SREQ/REG and every safety-relevant INPUT id.

**G4 — release** (the whole package)
- Is the technical-file index complete, and does every path in it exist?
- Do the draft declarations list the right acts and standards?
- Does the user information cover every residual risk?
- Run a claims audit over all documents.
- Does REPORT.md state the open NCs, the FAILs and the blocked items honestly?
- Does `python3 method/gates/dd_check.py --tier full` pass?

## Ending each gate
Your final message is what the lead receives. Its first line must be exactly:
`VERDICT G<n> r<k>: PASS|CONDITIONAL|VETO — open major: <ids or none>; open minor: <ids or none>`
Then add at most 10 lines: the reasons for the majors and what would close them.

## Boundaries
- No web access except the read-only spot checks.
- Never open stragowski.suby.pl.
- Never read outside the run directory except /usr, /opt and /etc.
- Never create files whose names start with REPORT, SUMMARY, FINDINGS or ANALYSIS, in any letter case.

## Time box
Use the time box in the gate request. Defaults: G0 ≤ 8 min, G1 ≤ 10 min, a re-check ≤ 5 min, G2–G4 ≤ 20 min.
agents/standards-researcher.md (68 wierszy)
---
name: standards-researcher
description: Researches ONE regulatory or market question for a hardware product from real web pages (legal acts, harmonised standards, national rules, safety listings, markings, substances, recalls, comparable products). Writes sourced rows for the compliance matrix and notes with excerpts. Launch one per question; several can run in parallel.
model: sonnet
---

You are a standards researcher in a hardware design run that puts compliance first. You answer ONE research
question from your brief, using facts you read on real pages. You do not design the product. You report what
the sources say, and the lead decides.

## Read first
- `method/HYGIENE.md` (binding).
- `method/templates/register-formats.md` (the matrix.csv format).
- Your brief.
- `INPUT/brief.md` and `INPUT/requirements.md`.

## How to research
- Use WebSearch and WebFetch. If they are not loaded, load them with ToolSearch (query
  `select:WebSearch,WebFetch`). `curl -sL` also works. If a site blocks reading, use another source.
- Prefer primary sources:
  - legal texts and official journals;
  - official lists of harmonised standards;
  - the regulator's own rule texts and guidance;
  - standards bodies' catalogue pages (scope, edition, year);
  - official recall and safety-alert databases;
  - manufacturers' datasheets and manuals.
- Use secondary sources (test labs, consultants) only for cost and time ranges, and say that they are
  secondary.
- Never rebuild a standard from memory. Cite a clause number only when a page you read shows it; otherwise
  name the topic. Full standard texts are paywalled, so write "full text not read".
- If you cannot confirm a fact, report it as not found. Never guess.

## Output (the exact paths are in your brief; create nothing else)
1. `<REG>/sources/R<n>-<slug>.csv`: matrix rows with the header from register-formats.md. Leave `id`
   empty. Write at most ~12 rows, the ones that drive the design first; put the minor duties of one act into
   one row.
   - `applies`: `yes`, `no` or `maybe`, decided from the scope texts, with the reason in `why`.
   - `trigger`: the feature tags from your brief.
   - `route` and `evidence_needed`.
   - `cost_eur` and `weeks`: ranges, labelled ASSUMPTION unless a page shows a price.
   - `source` and `read_date`.
   - Leave `hazards` and `derived_reqs` empty.
2. `<REG>/sources/R<n>-<slug>.md`, with three parts:
   - For each row: the URL, the read date, and a short verbatim excerpt (≤ 2 sentences) that supports
     `applies`, `route` and each design-driving clause.
   - "Hazards seen in sources": one line each with hazard, situation, harm and URL.
   - "Not found / uncertain".
3. Validate the CSV with `python3 method/gates/register.py lint <your csv>`. Fix it until there is no ERROR.

A field-evidence brief (recalls, incidents, comparable products) names extra outputs, such as a benchmark
table. Follow the brief.

## Boundaries
- Use the web read-only.
- Never contact anyone, sign up, log in, buy anything or request quotes.
- Never fetch, search for or open stragowski.suby.pl (any path, copy, cache or archive).
- Never read or list anything outside the run directory, except /usr, /opt, /etc and the toolchain paths in
  `INPUT/TOOLCHAIN.md`.
- Install nothing.
- Never create files whose names start with REPORT, SUMMARY, FINDINGS or ANALYSIS, in any letter case.

## Time and ending
Stop at the time box in your brief, even if the work is incomplete; write what you have first. Your final
message is your report, at most 15 lines:
- the files you wrote and the row count;
- the three facts that most drive the design, each with its source;
- the lookups that failed.
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 (159 wierszy)
# Decision policy

No human answers during the run, and this file takes the human's place. It binds the lead and every agent.
When this file does not settle a choice: take the option that is safer and easier to certify, write it down
as a decision with the alternatives you rejected, and continue.

## 1. Priorities when goals conflict
1. **People.**
   - No hazard is left with a residual risk ≥ 8.
   - Every hazard of severity ≥ 3 is removed by design, or covered by a protection that works without
     firmware.
2. **Legal sale in the target markets.** Every applicable obligation in the matrix has a route, and the
   design produces the evidence that route needs.
3. **The input requirements.** Thresholds before targets. Higher priority or weight wins.
4. **Unit cost and retail price.**
5. **Certification cost and time to market.**
6. **Everything else** (looks, extra features).

Record every conflict and how it was resolved in `01-requirements/requirements.md` under "Conflicts and
decisions".

## 2. Certifiable-architecture defaults
These are the starting rules for `arch-rules.md`. Deviate only with a recorded reason.
- **Energy.** Keep hazardous voltage out of the product when a certified external supply that is sold as a
  catalogue part can deliver the power; check real parts. Otherwise put mains inside a certified power
  module or a closed mains zone. Keep mains off the custom logic board whenever that is possible.
- **Radio.** None unless a requirement needs it. If one is needed, use a pre-certified module within its
  approval conditions (antenna, layout, host labelling) in every target market.
- **Protection.** Give every hazard of severity ≥ 3 a hardware protection: a fuse, thermal cut-off,
  interlock or current limit. Firmware is a second layer and never the only one. Loss of power or of control
  leads to a safe state.
- **Materials.** Choose flammability class, temperature rating and substance status (RoHS, REACH) to suit
  where each part sits. Use parts that come with the documents a lab will ask for.
- **Batteries.** None unless needed. If needed, use certified cells or packs with their transport-test
  documents.
- **Sub-assemblies.** Prefer catalogued, certified ones (supplies, switches, cords, connectors, modules).
  Their approvals carry over into the technical file.
- **Information for use** is the last of the three steps. It is never the main measure against a hazard of
  severity ≥ 3.

## 3. Risk scale (ISO 12100 manner)
**Severity S**
- 1: minor injury (first aid) or small material damage.
- 2: reversible injury that needs treatment.
- 3: serious or irreversible injury, or a fire that leaves the product.
- 4: death, several serious injuries, or a fire in the building.

**Probability P**, over the life of all units sold
- 1: improbable; needs several independent faults.
- 2: remote; a single fault plus an unusual condition.
- 3: occasional; a single fault or foreseeable misuse.
- 4: likely; normal use or common misuse.

**Risk R = S × P**
- R ≥ 8 is unacceptable: redesign.
- R 5–7: reduce it further, unless that is impracticable; write why.
- R ≤ 4 is acceptable.
- Severity 3–4 always needs an `I:` measure or a `P:` measure that does not depend on firmware.

## 4. Markets, currency, language
- **Markets.** The ones the inputs name. If the inputs name none, the EU and the US.
- **Currency.** EUR. When you convert, state the exchange rate and its date.
- **Language.** English for every file.

## 5. Concept scoring (`method/gates/concept_score.py`)
- **Knock-outs come first.** A concept is out if it:
  - is predicted to miss a requirement threshold by more than the estimate's uncertainty; or
  - breaks an architecture rule marked mandatory.
- **Weights.**
  - Requirement criteria (`req`) carry 75 % of the weight, shared in proportion to the requirement priorities
    or weights.
  - Certification criteria carry 25 %: cert-risk 10, cert-cost 8, cert-time 7, as raw weights.
- **cert-risk** is scored from 1 to 5 (5 = lowest risk). Its basis is a count of these risk factors:
  - an item that needs a notified body;
  - an intentional radio that is not a pre-certified module;
  - hazardous voltage on a custom board;
  - a safety function that depends on software;
  - a product family without a harmonised standard;
  - an open question about a material or substance.
- **cert-cost and cert-time** are computed by the script from the certification bill (the matrix rows each
  concept triggers), on a ratio scale, and the sensitivity tests each bill's low and high bound. Never score
  them by hand.
- **Order.** Write the weights before the scores, and never change a weight after you have seen the totals.
  If you must change one, record why in `concept.md`.
- **Ties.** If the winner is within 3 % of the runner-up, choose the one with the lower certification risk,
  and record it.
- **Show the difference.** Always report the winner with the certification weight set to 0, so the reader
  can see what the compliance-first stance changed.

## 6. Conformity assessment
**Gates**
- G0: regulatory envelope.
- G1: concept freeze.
- G2: architecture (full tier).
- G3: detailed design (full tier).
- G4: release (full tier).

**Major NC.** Any of the following:
- an applicable legal obligation or harmonised-standard requirement that is missing, or wrongly marked as not
  applicable;
- a hazard with R ≥ 8;
- a hazard of severity ≥ 3 that is neither removed by design nor covered by a protection independent of
  firmware;
- a likely serious hazard missing from the register;
- the chosen concept or design breaks a mandatory architecture rule;
- a conformity, certification or listing claim;
- a `met` or PASS that its evidence does not support;
- a conformity route that is not available for this product;
- a design-driving SOURCE that does not say what is claimed;
- scoring without certification criteria, or with weights tuned after the scores.

**Minor NC.** Anything else that weakens the file: a missing read date, a weak justification, a gap in the
trace, an undecided `maybe`, a document that is incomplete but not misleading.

**Verdicts**
- VETO when any major NC is open.
- CONDITIONAL when only minors are open. At the full tier, close them by the next gate. At the concept tier,
  list the minors still open after G1 in `REPORT.md` as open issues.
- PASS when nothing is open.

**Closing NCs**
- The lead closes NCs with evidence. Only the assessor sets `closed`.
- Each gate gets at most 1 re-check at the concept tier and 2 at the full tier. G0 closures are re-checked
  inside G1.
- When the re-checks are used up and a major NC is still open, switch concept if that closes it.
  Otherwise set the NC to `unresolved`, with the reason in the note, and carry it as a FAIL. `register.py`
  accepts `unresolved` only after a re-check has kept the NC open. Then:
  - in `compliance.csv`, the affected ids are `partly` or `not_met`;
  - `REPORT.md` has an "Unresolved nonconformities" section;
  - `manifest.json` → `blocked[]` gets an entry if a human must act.
- The lead never edits an NC's text, a verdict or the assessor log.

## 7. Honesty about compliance
- **Never write** that the product is certified, approved, listed, compliant or CE-marked. Write "designed
  to <standard or clause>; route: <route>; evidence: <path>; not yet assessed by a lab".
- **Standard texts are paywalled.** Quote clause numbers only from sources you have read. Buying the
  standards goes into `blocked[]`.
- **Certification-type requirements** are `met` only if the requirement's own metric is design evidence
  that exists in the package. Otherwise they are `partly`, with the note "route identified; lab tests NOT
  RUN".
- **Steps only a human can take** go into `blocked[]` with the reason:
  - buying standards;
  - work by an accredited lab or a notified body;
  - producer registrations and legal representatives;
  - physical tests.

## 8. Time and stop rules
The clock plan is in the main prompt (§4 and §6); `STATE.md` has its absolute times. When you are behind, cut
depth, never honesty.

Stop rules:
- **A lookup fails twice.** Use a labelled ASSUMPTION range and note it.
- **A tool route fails three honest attempts.** Use a labelled fallback, and add a `blocked[]` entry if the
  result mattered.
- **A subagent is 10 minutes past its time box.** Continue without it: use what it wrote, and relaunch it
  once if its result matters. Never wait for a researcher past T+22.
- **Concept tier.** At T+75 stop all analysis and finish the package. Never go past T+100.
- **Full tier.** At T+10:00 stop new design work and finish.
files/method/gates/concept_score.py (268 wierszy)
#!/usr/bin/env python3
"""concept_score.py - concept selection with certification as an explicit criterion.

Computes, per concept, the certification bill (cost and time of the matrix rows its feature tags trigger),
applies the knock-outs, scores requirement and certification criteria with fixed weights, and tests the
winner's sensitivity: each weight x0.8 and x1.2, certification share 0 and 0.4, every score range (a knock-out
range can revive a concept) and each concept's bill at its low and high bound.
cert-cost and cert-time use a ratio scale over the concepts that survive the knock-outs:
score = 5 - 4 * (bill - lowest bill) / highest bill, so a bill 1 % above the cheapest loses only 0.04 points.

Run from the run directory after filling deliverables/02-concept/scoring/{concepts,criteria,scores}.csv
(formats: method/templates/register-formats.md):
    python3 method/gates/concept_score.py [--cert-share 0.25]
Writes scoring/result.json and scoring/result.md. Exit 0 = computed, 1 = input errors. Standard library only.
"""
import argparse
import csv
import io
import json
import os
import re
import sys

REG = os.path.join("deliverables", "02-concept", "compliance")
SCORING = os.path.join("deliverables", "02-concept", "scoring")
MATRIX = ["id", "jurisdiction", "item", "applies", "why", "trigger", "driving_clauses", "hazards",
          "derived_reqs", "evidence_needed", "route", "cost_eur", "weeks", "source", "read_date"]
AUTO = ("cert-cost", "cert-time")


def read(path, header, errors):
    if not os.path.isfile(path):
        errors.append(f"{path} 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]] != header:
        errors.append(f"{path}: header must be exactly {','.join(header)}")
        return []
    out = []
    for n, row in enumerate(rows[1:], start=2):
        if not any(c.strip() for c in row):
            continue
        if len(row) != len(header):
            errors.append(f"{path} line {n}: {len(row)} fields, expected {len(header)}")
            continue
        out.append(dict(zip(header, [c.strip() for c in row]), _line=n))
    return out


def rng(text):
    t = (text or "").replace(",", "").replace("€", "").replace("EUR", "").strip()
    t = re.sub(r"(\d+(?:\.\d+)?)\s*k\b", lambda m: str(float(m.group(1)) * 1000), t, flags=re.I)
    t = t.lstrip("~≈ ")
    t = re.sub(r"\s*[A-Za-z][A-Za-z .]*$", "", t) if t[:1].isdigit() else t
    m = re.match(r"^(\d+(?:\.\d+)?)\s*(?:-|–|to)\s*(\d+(?:\.\d+)?)$", t)
    if m:
        return sorted((float(m.group(1)), float(m.group(2))))
    return [float(t), float(t)] if re.match(r"^\d+(?:\.\d+)?$", t) else [0.0, 0.0]


def tags(text):
    return {t.strip().lower() for t in re.split(r"[;,]", text or "") if t.strip()}


def bill(matrix, features):
    cost, weeks, rows = [0.0, 0.0], [0.0, 0.0], []
    for r in matrix:
        ap = r["applies"].lower()
        trig = tags(r["trigger"]) or {"always"}
        if ap not in ("yes", "maybe") or ("always" not in trig and not trig & features):
            continue
        c, w = rng(r["cost_eur"]), rng(r["weeks"])
        if ap == "yes":
            cost[0] += c[0]
            weeks[0] = max(weeks[0], w[0])
        cost[1] += c[1]
        weeks[1] = max(weeks[1], w[1])
        rows.append(r["id"] + ("" if ap == "yes" else "?"))
    return {"cost_eur": cost, "weeks": weeks, "mid_cost_eur": sum(cost) / 2, "mid_weeks": sum(weeks) / 2,
            "rows": rows}


def cert_scores(bills, alive, pick=None):
    """cert-cost and cert-time of the live concepts on the ratio scale. pick = (concept, 0|1) puts that concept's
    bill at its low bound (yes rows, low ends) or high bound (yes + maybe rows, high ends) instead of the middle."""
    out = {c: {} for c in alive}
    for k, mid, bound in (("cert-cost", "mid_cost_eur", "cost_eur"), ("cert-time", "mid_weeks", "weeks")):
        vals = {c: bills[c][bound][pick[1]] if pick and pick[0] == c else bills[c][mid] for c in alive}
        lo, hi = min(vals.values(), default=0.0), max(vals.values(), default=0.0)
        for c, v in vals.items():
            out[c][k] = round(5.0 - 4.0 * (v - lo) / hi, 3) if hi > 0 else 5.0
    return out


def evaluate(alive, crit, raw_w, scores, share, bills, pick=None):
    auto = cert_scores(bills, alive, pick)
    sc = {c: {**scores[c], **{k: v for k, v in auto[c].items() if k in crit}} for c in alive}
    req = {k: raw_w[k] for k in crit if crit[k] == "req"}
    cert = {k: raw_w[k] for k in crit if crit[k] == "cert"}
    w = {k: (1 - share) * v / sum(req.values()) for k, v in req.items()}
    w.update({k: share * v / sum(cert.values()) for k, v in cert.items()})
    totals = {c: sum(w[k] * sc[c][k] for k in w) for c in alive}
    return totals, sorted(alive, key=lambda c: (-totals[c], c))


def main():
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("--cert-share", type=float, default=0.25, help="weight share of certification criteria")
    args = ap.parse_args()
    errors, warns = [], []
    matrix = read(os.path.join(REG, "matrix.csv"), MATRIX, errors)
    concepts = read(os.path.join(SCORING, "concepts.csv"), ["concept", "name", "features"], errors)
    criteria = read(os.path.join(SCORING, "criteria.csv"), ["criterion", "kind", "weight", "source"], errors)
    scores_in = read(os.path.join(SCORING, "scores.csv"), ["concept", "criterion", "score", "low", "high", "basis"], errors)
    names = {c["concept"]: c["name"] for c in concepts}
    if len(names) < 2:
        errors.append("concepts.csv: the method compares at least 2 concepts (3 by default)")
    crit, raw_w = {}, {}
    for c in criteria:
        k, kind = c["criterion"], c["kind"].lower()
        if kind not in ("req", "cert", "ko"):
            errors.append(f"criteria.csv line {c['_line']}: kind '{c['kind']}' is not req|cert|ko")
            continue
        crit[k] = kind
        if kind != "ko":
            try:
                raw_w[k] = float(c["weight"])
                assert raw_w[k] > 0
            except (ValueError, AssertionError):
                errors.append(f"criteria.csv line {c['_line']}: weight '{c['weight']}' must be a number > 0")
    if not any(v == "req" for v in crit.values()):
        errors.append("criteria.csv: no req criterion")
    if not any(v == "cert" for v in crit.values()):
        errors.append("criteria.csv: no cert criterion (cert-risk, cert-cost, cert-time); the method requires them")
    feats = {c["concept"]: tags(c["features"]) for c in concepts}
    known = set().union(*[tags(r["trigger"]) for r in matrix]) if matrix else set()
    for c, f in feats.items():
        unknown = sorted(f - known)
        if unknown:
            warns.append(f"concept {c}: feature tags {unknown} trigger no matrix row")
    bills = {c: bill(matrix, feats[c]) for c in names}
    scores = {c: {} for c in names}
    ranges = []
    for s in scores_in:
        c, k = s["concept"], s["criterion"]
        where = f"scores.csv line {s['_line']}"
        if c not in names or k not in crit:
            errors.append(f"{where}: unknown concept '{c}' or criterion '{k}'")
            continue
        if k in AUTO:
            warns.append(f"{where}: {k} is computed from the certification bill; your score is ignored")
            continue
        try:
            v = float(s["score"])
        except ValueError:
            errors.append(f"{where}: score '{s['score']}' is not a number")
            continue
        if crit[k] == "ko" and v not in (0.0, 1.0):
            errors.append(f"{where}: a ko score is 1 (pass) or 0 (fail)")
        if crit[k] != "ko" and not 1 <= v <= 5:
            errors.append(f"{where}: score must be 1-5")
        if not s["basis"]:
            errors.append(f"{where}: basis empty (give the number behind the score and its origin)")
        elif not re.search(r"\d|/", s["basis"]):
            warns.append(f"{where}: basis has no number or path")
        scores[c][k] = v
        if s["low"] or s["high"]:
            try:
                ranges.append((c, k, float(s["low"] or v), float(s["high"] or v)))
            except ValueError:
                errors.append(f"{where}: low/high must be numbers")
    for c in names:
        for k in crit:
            if k not in scores[c] and k not in AUTO:
                errors.append(f"scores.csv: no score for concept {c}, criterion {k}")
    if errors:
        for e in errors:
            print(f"ERROR {e}")
        print(f"concept_score: errors={len(errors)} -> FAIL (nothing written)")
        return 1
    ko = {c: [k for k in crit if crit[k] == "ko" and scores[c][k] == 0] for c in names}
    alive = [c for c in names if not ko[c]]
    shown = cert_scores(bills, alive or list(names))  # knocked-out concepts get no cert-cost/cert-time score
    for c in names:
        scores[c].update({k: shown[c][k] if c in shown else None for k in AUTO if k in crit})
    result = {"cert_share": args.cert_share, "cert_criteria": [k for k in crit if crit[k] == "cert"],
              "weights_raw": raw_w, "knocked_out": {c: v for c, v in ko.items() if v}, "bills": bills,
              "scores": scores, "winner": None}
    if alive:
        totals, ranking = evaluate(alive, crit, raw_w, scores, args.cert_share, bills)
        win = ranking[0]
        margin = (totals[win] - totals[ranking[1]]) / totals[win] if len(ranking) > 1 else None
        flips = []
        for k in raw_w:
            for f in (0.8, 1.2):
                r2 = evaluate(alive, crit, {**raw_w, k: raw_w[k] * f}, scores, args.cert_share, bills)[1]
                if r2[0] != win:
                    flips.append(f"weight {k} x{f} -> {r2[0]}")
        for c, k, lo, hi in ranges:
            for v in (lo, hi):
                s2 = {x: dict(scores[x]) for x in scores}
                s2[c][k] = v
                alive2 = [x for x in names if not any(crit[j] == "ko" and s2[x][j] == 0 for j in crit)]
                if alive2:
                    r2 = evaluate(alive2, crit, raw_w, s2, args.cert_share, bills)[1]
                    if r2[0] != win:
                        flips.append(f"score {c}/{k}={v:g} -> {r2[0]}")
        for c in (alive if len(alive) > 1 and any(k in crit for k in AUTO) else []):
            for b, label in ((0, "low"), (1, "high")):
                r2 = evaluate(alive, crit, raw_w, scores, args.cert_share, bills, (c, b))[1]
                if r2[0] != win:
                    flips.append(f"bill of {c} at its {label} bound -> {r2[0]}")
        shares = {str(sh): evaluate(alive, crit, raw_w, scores, sh, bills)[1][0] for sh in (0.0, 0.4)}
        tie = margin is not None and margin < 0.03
        tie_pick = None
        if tie and "cert-risk" in crit:
            tie_pick = max(ranking[:2], key=lambda c: (scores[c]["cert-risk"], -ranking.index(c)))
        result.update({"totals": {c: round(totals[c], 3) for c in alive}, "ranking": ranking, "winner": win,
                       "margin": None if margin is None else round(margin, 4), "tie": tie,
                       "tie_break_lower_cert_risk": tie_pick, "winner_without_cert": shares["0.0"],
                       "winner_cert_share_0.4": shares["0.4"], "flips": flips})
    os.makedirs(SCORING, exist_ok=True)
    with open(os.path.join(SCORING, "result.json"), "w", encoding="utf-8") as fh:
        json.dump(result, fh, indent=2)
    md = ["# Concept scoring (generated by method/gates/concept_score.py; edit the CSV inputs, not this file)", "",
          f"Certification criteria share {args.cert_share:.0%} of the weight: {', '.join(result['cert_criteria'])}.",
          "Certification bill = the matrix rows each concept triggers (cost: sum, yes rows .. yes+maybe rows; "
          "time: the longest single item, assuming parallel campaigns). ASSUMPTION ranges unless the matrix "
          "cites a price.", "", "| concept | name | knocked out by | cert cost EUR | cert weeks | rows | total |",
          "|---|---|---|---|---|---|---|"]
    for c in names:
        b = bills[c]
        md.append(f"| {c} | {names[c]} | {', '.join(ko[c]) or '-'} | {b['cost_eur'][0]:.0f}-{b['cost_eur'][1]:.0f} | "
                  f"{b['weeks'][0]:g}-{b['weeks'][1]:g} | {' '.join(b['rows'])} | "
                  f"{result.get('totals', {}).get(c, '-')} |")
    md += ["", "| criterion | kind | raw weight | " + " | ".join(names) + " |", "|---|---|---|" + "---|" * len(names)]
    for k in crit:
        md.append(f"| {k} | {crit[k]} | {raw_w.get(k, '-')} | " + " | ".join(
            "-" if scores[c][k] is None else f"{scores[c][k]:g}" for c in names) + " |")
    if any(k in crit for k in AUTO):
        mids = {c: (bills[c]["mid_cost_eur"], bills[c]["mid_weeks"]) for c in (alive or names)}
        top, dc = max(v[0] for v in mids.values()), max(v[0] for v in mids.values()) - min(v[0] for v in mids.values())
        dw = max(v[1] for v in mids.values()) - min(v[1] for v in mids.values())
        md += ["", "cert-cost and cert-time: ratio scale 5 - 4 x (bill - lowest) / highest over the mid bills ("
               + "; ".join(f"{c} {v[0]:.0f} EUR, {v[1]:g} weeks" for c, v in mids.items())
               + f"). Largest difference: {dc:.0f} EUR ({dc / top if top else 0:.1%} of the highest bill) and {dw:g} weeks."]
    if result["winner"]:
        runner = (f"margin {result['margin']:.1%} over the runner-up" if result["margin"] is not None
                  else "no runner-up: the only concept that survives the knock-outs")
        md += ["", f"Winner: **{result['winner']}** ({runner}"
               f"{'; TIE: POLICY picks the lower certification risk: ' + str(result['tie_break_lower_cert_risk']) if result['tie'] else ''}).",
               f"Winner with certification weight 0: {result['winner_without_cert']}; with share 0.4: "
               f"{result['winner_cert_share_0.4']}.",
               "Sensitivity flips: " + ("; ".join(result["flips"]) if result["flips"] else "none (the winner holds)") + "."]
    else:
        md += ["", "NO WINNER: every concept is knocked out. Revise the concepts."]
    with open(os.path.join(SCORING, "result.md"), "w", encoding="utf-8") as fh:
        fh.write("\n".join(md) + "\n")
    for w in warns:
        print(f"WARN  {w}")
    print("\n".join(md[5:]))
    print(f"concept_score: winner={result['winner']} -> wrote {SCORING}/result.json and result.md")
    return 0


if __name__ == "__main__":
    sys.exit(main())
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/gates/register.py (680 wierszy)
#!/usr/bin/env python3
"""register.py - compliance register tool (formats: method/templates/register-formats.md).

Run from the run directory; standard library only; exit 0 = no ERROR. REG = deliverables/02-concept/compliance.
  init --tier T [--start HH:MM]   register, scoring files, compliance.csv + manifest skeletons, STATE.md clock plan
  lint FILE.csv                   validate a matrix-format CSV (researcher rows or matrix.csv)
  merge                           append new REG/sources/*.csv rows to REG/matrix.csv with new M- ids; never rewrites
                                  a row, refuses while matrix.csv does not parse; provenance in REG/merge-log.csv
  check --tier T [--gate Gn]      register, traces, NCs, claims; --gate makes the gate verdict binding
  status | next-nc                open items | next free NC id
  nc-status NC-nnn S --by lead|assessor --note TEXT
                                  lead: closure-proposed (note names evidence files inside deliverables/) |
                                  unresolved (only after a re-check kept the NC open); assessor: closed | open
  render [--out PATH]             markdown tables of the register (default REG/register.md)
  manifest --tier T               add unlisted deliverable files to deliverables/manifest.json
"""
import argparse
import csv
import datetime
import glob
import io
import json
import os
import re
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
    from dd_check import requirement_ids  # same INPUT id parser as the package checker
except Exception:  # noqa: BLE001
    def requirement_ids(path):
        text = open(path, encoding="utf-8", errors="replace").read() if os.path.isfile(path) else ""
        return list(dict.fromkeys(re.findall(r"(?m)^#{1,6}\s+([A-Z][A-Z0-9]{1,7}-[A-Z0-9]{1,7}-\d{1,4})\b", text)))

D = "deliverables"
SCORING = os.path.join(D, "02-concept", "scoring")
TF_INDEX = os.path.join(D, "08-safety-certification", "technical-file", "index.md")
MATRIX = ["id", "jurisdiction", "item", "applies", "why", "trigger", "driving_clauses", "hazards",
          "derived_reqs", "evidence_needed", "route", "cost_eur", "weeks", "source", "read_date"]
HAZARDS = ["id", "phase", "hazard", "situation", "harm", "severity", "probability", "risk", "measures",
           "fw_independent", "res_severity", "res_probability", "res_risk", "sreq", "matrix_refs"]
LOG = ["source", "line", "jurisdiction", "item", "id"]
GATES = ("G0", "G1", "G2", "G3", "G4")
NC_STATUS = ("open", "closure-proposed", "closed", "unresolved")
MAY_SET = {"lead": ("closure-proposed", "unresolved"), "assessor": ("closed", "open")}
ID = r"(?<![\w-])"  # an id never starts inside another id: TST-REG-01 holds no REG-01
DERIVED = re.compile(ID + r"((?:SREQ|REG)-\d{2,3})\b")
CLAIM = re.compile(r"\b(?:is|are|was|were|be|been|being|fully|already)\s+(?:(?:not|never|yet|fully|already|now)\s+)*"
                   r"(?:certified|compliant|conformant|CE[- ]marked)\b|\bcertified (?:product|device|design|unit)\b"
                   r"|\b(?:CE|UL|ETL|CSA|FCC|TÜV|NRTL)[- ](?:certified|approved|listed|compliant|marked)\b"
                   r"|\b(?:conforms?|complies|comply) (?:to|with)\b(?=[^.;]{0,40}?\b(?:EN|IEC|ISO|UL|CSA|FCC|"
                   r"directive|regulation|\d{4}/\d+/EU)\b)", re.I)
NEG = {"not", "never", "no", "nothing", "none", "nor", "neither", "cannot", "without"}
MODAL = {"will", "would", "shall", "should", "must", "can", "could", "may", "might", "to", "until", "once", "if",
         "unless", "before", "after", "pending", "planned", "plan", "target", "goal", "aim", "intended", "expected"}
PART = re.compile(r"\b(?:supply|supplies|adapter|psu|module|cell|pack|battery|charger|cord|cable|plug|inlet|switch|fuse|"
                  r"cut-?off|thermostat|connector|relay|motor|fan|transformer|component|part|sub-?assembly|driver|"
                  r"material)s?\b", re.I)
DELIV_PATH = re.compile(r"(?<![\w./-])(?:deliverables/)?(?:0[1-9]|1[01])-[a-z][\w.-]*(?:/[\w.-]*)*")
OTHER_PATH = re.compile(r"(?<![\w./-])(?:[\w.-]+/)+[\w.-]*\.(?:md|csv|json|py|step|stp|pdf|svg|dxf|ini|png|glb|txt|"
                        r"ya?ml|kicad_\w+|zip|html?)\b")
SCHEDULE = {"concept": [(0, "start; init; 4 researchers launched in one message"),
                        (15, "physics-map.md, hazards.csv; concept.md sections 1 and 4 drafted"),
                        (22, "matrix merged and decided; arch-rules.md; G0 launched (wait for no researcher past this)"),
                        (32, "01-requirements (given + REG/SREQ/DR), traces clean; concept.md 2-3; G0 NCs answered"),
                        (42, "concepts, calc/, scoring, concept_score.py; G1 requested"),
                        (52, "G1 ruled; concept.md 5-12, compliance.csv, REPORT.md drafted; re-check requested if VETO"),
                        (60, "TARGET END: package complete, checks run"),
                        (75, "LATE: stop analysis, finish with what exists"), (100, "LAST MOMENT (hard stop 2 h)")],
            "full": [(0, "start; as the concept tier"), (75, "concept phase done (01, 02, REPORT v1, manifest); G1"),
                     (135, "architecture.md + technical-file index; G2"), (375, "detailed design done (03-11)"),
                     (435, "G3 closed"), (510, "integration, G4, dd_check full PASS; finish"),
                     (600, "LATE: stop new work, finish")]}


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

    def emit(self, name):
        for tag, msgs in (("NOTE ", self.notes), ("WARN ", self.warnings), ("ERROR", self.errors)):
            for m in msgs:
                print(f"{tag} {m}")
        print(f"{name}: errors={len(self.errors)} warnings={len(self.warnings)} -> {'FAIL' if self.errors else 'PASS'}")
        return 1 if self.errors else 0


def text_of(path):
    return open(path, encoding="utf-8", errors="replace").read() if os.path.isfile(path) else ""


def read_csv(path, header, rep, label):
    if not os.path.isfile(path):
        rep.error(f"{label}: missing ({path})")
        return None
    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]] != header:
        rep.error(f"{label}: header must be exactly {','.join(header)}")
        return None
    out = []
    for n, row in enumerate(rows[1:], start=2):
        if any(c.strip() for c in row):
            if len(row) != len(header):
                rep.error(f"{label} line {n}: {len(row)} fields, expected {len(header)} (quote fields with commas)")
            else:
                out.append(dict(zip(header, [c.strip() for c in row]), _line=n))
    return out


def append_csv(path, header, rows):
    """Append rows (header first if the file is new or empty); existing lines are never rewritten."""
    fresh = not os.path.isfile(path) or not os.path.getsize(path)
    if not fresh and not rows:
        return
    sep = ""
    if not fresh:
        with open(path, "rb") as fh:
            fh.seek(-1, 2)
            sep = "" if fh.read(1) in (b"\n", b"\r") else "\n"
    with open(path, "a", encoding="utf-8", newline="") as fh:
        fh.write(sep)
        w = csv.writer(fh, lineterminator="\n")
        if fresh:
            w.writerow(header)
        w.writerows([r.get(h, "") for h in header] for r in rows)


def parse_range(text):
    """'1500' | '1-3' | '2k-5k' | '1,000-2,000' -> (lo, hi); '' -> None; other -> 'bad'."""
    t = re.sub(r"(\d+(?:\.\d+)?)\s*k\b", lambda m: str(float(m.group(1)) * 1000),
               (text or "").replace(",", "").replace("€", "").replace("EUR", "").strip(), flags=re.I)
    t = t.lstrip("~≈ ")
    t = re.sub(r"\s*[A-Za-z][A-Za-z .]*$", "", t) if t[:1].isdigit() else t  # '2-4 weeks' -> '2-4'
    m = re.match(r"^(\d+(?:\.\d+)?)\s*(?:-|–|to)\s*(\d+(?:\.\d+)?)$", t)
    if m:
        return tuple(sorted((float(m.group(1)), float(m.group(2)))))
    return (float(t), float(t)) if re.match(r"^\d+(?:\.\d+)?$", t) else (None if not t else "bad")


def lint_matrix(rows, rep, label, need_ids):
    seen = {}
    for r in rows:
        at, ap = f"{label} line {r['_line']} ({r['item'][:40]})", r["applies"].lower()
        if need_ids:
            if not re.fullmatch(r"M-\d{2,3}", r["id"]):
                rep.error(f"{at}: id '{r['id']}' is not M-nn (give a hand-added row the next free M- number)")
            elif r["id"] in seen:
                rep.error(f"{at}: duplicate id {r['id']}")
            seen[r["id"]] = r
        if not r["item"] or not r["why"]:
            rep.error(f"{at}: item and why are required")
        if ap not in ("yes", "no", "maybe"):
            rep.error(f"{at}: applies '{r['applies']}' is not yes|no|maybe")
        url = re.search(r"https?://", r["source"])
        if ap in ("yes", "maybe"):
            if not url:
                rep.error(f"{at}: no source URL")
            elif not re.match(r"^\d{4}-\d{2}-\d{2}$", r["read_date"]):
                rep.error(f"{at}: read_date '{r['read_date']}' is not YYYY-MM-DD")
            empty = [c for c in ("route", "evidence_needed", "trigger", "cost_eur", "weeks") if not r[c]]
            if empty:
                rep.warn(f"{at}: empty {', '.join(empty)}")
            for c in ("cost_eur", "weeks"):
                if parse_range(r[c]) == "bad":
                    rep.error(f"{at}: {c} '{r[c]}' is not a number or a range a-b")
        elif ap == "no" and not url:
            rep.warn(f"{at}: 'no' without a source for the scope decision")
    return seen


def load_ncs(reg):
    ncs = []
    for path in sorted(glob.glob(os.path.join(reg, "nc", "NC-*.md"))):
        text = text_of(path)
        head = {}
        for line in text.splitlines()[:15]:
            m = re.match(r"^[\W_]*(severity|gate|status|refs|raised)\s*:\s*(.*)$", line.strip(), re.I)
            if m:
                head.setdefault(m.group(1).lower(), m.group(2).strip(" *"))
        parts = re.split(r"(?m)^##\s+(.+?)\s*$", text)
        ncs.append({"id": os.path.basename(path)[:-3], "title": (text.strip().splitlines() or [""])[0].lstrip("# "),
                    "severity": head.get("severity", "").lower(), "gate": head.get("gate", "").upper(),
                    "status": head.get("status", "").lower(), "refs": head.get("refs", ""),
                    "sec": {parts[i].strip().lower(): parts[i + 1].strip() for i in range(1, len(parts) - 1, 2)}})
    return ncs


def gate_rounds(reg, gate):
    out = []
    for f in glob.glob(os.path.join(reg, "gates", f"{gate}-r*.md")):
        m = re.search(r"-r(\d+)\.md$", f)
        v = re.search(r"(?im)^[\W_]*verdict\s*:\s*\**\s*(PASS|CONDITIONAL|VETO)\b", text_of(f))
        if m:
            out.append((int(m.group(1)), v.group(1).upper() if v else None))
    return sorted(out)


def is_claim(line):
    """A conformity claim that no negation or modal governs and that is not about a purchased part (POLICY 7)."""
    for m in CLAIM.finditer(line):
        pre = line[:m.start()]
        words = re.findall(r"[\w']+", re.split(r"[.;:!?()]|,\s", pre)[-1].lower())
        inner = re.findall(r"[\w']+", m.group(0).lower())
        if any(w in NEG or w.endswith("n't") for w in words + inner) or MODAL & set(words[-3:]):
            continue
        subj = pre[:pre.rfind("(")] if pre.count("(") > pre.count(")") else re.split(r"[.;:!?]|,\s", pre)[-1]
        if PART.search(" ".join(subj.split()[-4:] + re.split(r"[.;:!?(),]", line[m.end():])[0].split()[:3])):
            continue
        return True
    return False


def check_hazards(rows, rep, report_text):
    ids, phases = {}, set()
    for r in rows:
        hid, meas, fw = r["id"], r["measures"], r["fw_independent"].lower()
        at = f"hazards.csv line {r['_line']} ({hid})"
        if not re.fullmatch(r"H-\d{2,3}", hid) or hid in ids:
            rep.error(f"{at}: id must be a unique H-nn")
        ids[hid] = r
        phases.add(r["phase"].lower())
        if not (r["hazard"] and r["situation"] and r["harm"]):
            rep.error(f"{at}: hazard, situation and harm are required")
        try:
            s, p, rk, rs, rp, rr = (int(r[c]) for c in HAZARDS[5:8] + HAZARDS[10:13])
        except ValueError:
            rep.error(f"{at}: severity, probability, risk and res_* must be integers")
            continue
        if not all(1 <= v <= 4 for v in (s, p, rs, rp)):
            rep.error(f"{at}: severity and probability must be 1-4")
        if rk != s * p or rr != rs * rp:
            rep.error(f"{at}: risk must be severity x probability (initial and residual)")
        if rr > rk:
            rep.warn(f"{at}: residual risk above the initial risk")
        if fw not in ("yes", "no", "n/a"):
            rep.error(f"{at}: fw_independent '{r['fw_independent']}' is not yes|no|n/a")
        if rk >= 5 and not meas:
            rep.error(f"{at}: risk {rk} without measures")
        elif meas and not re.search(r"\b[IPU]:", meas):
            rep.warn(f"{at}: measures not tagged I:/P:/U:")
        if s >= 3 and (fw == "no" or (fw == "n/a" and not re.search(r"\bI:", meas))
                       or (fw == "yes" and not re.search(r"\b[IP]:", meas))):
            rep.error(f"{at}: severity {s} needs an I: measure or a P: measure independent of firmware (POLICY 3)")
        if rr >= 8:
            carried = re.search(ID + re.escape(hid) + r"\b", report_text)
            (rep.warn if carried else rep.error)(f"{at}: residual risk {rr} unacceptable (POLICY 3)"
                                                 + ("; carried as FAIL in REPORT.md" if carried else
                                                    "; redesign, or carry it as FAIL in REPORT.md"))
        if rk >= 5 and not DERIVED.search(r["sreq"]):
            rep.error(f"{at}: risk {rk} without an SREQ- id")
    if rows and (len(rows) < 8 or len(phases) < 3):
        rep.warn(f"hazards.csv: {len(rows)} hazards in {len(phases)} phases; walk every lifecycle phase and hazard type")
    return ids


def check_arch_rules(path, rep, known):
    text, rules = text_of(path), {}
    if not re.search(r"(?im)^\W*feature tags\s*:", text):
        rep.warn("arch-rules.md: no 'Feature tags:' line")
    parts = re.split(r"(?m)^#{2,4}\s+(AR-\d{2,3})\b", text)
    for i in range(1, len(parts) - 1, 2):
        rid, body = parts[i], parts[i + 1]
        basis = re.search(r"(?im)^[\W_]*basis\s*:\s*(.+)$", body)
        refs = re.findall(ID + r"([MH]-\d{2,3})\b", basis.group(1) if basis else "")
        mand = re.search(r"(?im)^[\W_]*mandatory\s*:\s*(yes|no)\b", body)
        if rid in rules or not refs or not mand:
            rep.error(f"arch-rules.md {rid}: needs a unique id, 'basis:' with M-/H- ids and 'mandatory: yes|no'")
        if [x for x in refs if x not in known]:
            rep.error(f"arch-rules.md {rid}: basis names unknown ids {[x for x in refs if x not in known]}")
        if not re.search(r"(?im)^[\W_]*check\s*:\s*\S", body):
            rep.warn(f"arch-rules.md {rid}: 'check:' missing")
        rules[rid] = {"title": (body.strip().splitlines() or [""])[0].strip(" -—:"),
                      "mandatory": bool(mand and mand.group(1).lower() == "yes")}
    if not rules:
        rep.error("arch-rules.md: no rules (sections '## AR-01 — ...')")
    return rules


def evidence_paths(note):
    """The tokens of a note that name an existing file inside deliverables/."""
    found = []
    for tok in re.findall(r"[\w.\-/]+", note):
        t = tok.rstrip(".,;:")
        t = os.path.relpath(t, os.path.abspath(D)) if t.startswith("/") else re.sub(r"^(?:\./)?(?:deliverables/)?", "", t)
        if t and ".." not in t.split("/") and os.path.isfile(os.path.join(D, t)):
            found.append(t)
    return found


def cmd_check(a, rep):
    reg, gate, late = a.reg, a.gate, a.gate in GATES[1:]
    report_text = text_of(os.path.join(D, "REPORT.md"))
    matrix = read_csv(os.path.join(reg, "matrix.csv"), MATRIX, rep, "matrix.csv") or []
    mids = lint_matrix(matrix, rep, "matrix.csv", need_ids=True)
    hazards = read_csv(os.path.join(reg, "hazards.csv"), HAZARDS, rep, "hazards.csv") or []
    hids = check_hazards(hazards, rep, report_text)
    if not matrix or not hazards:
        rep.error("matrix.csv and hazards.csv must both have rows")
    elif sum(r["applies"].lower() == "yes" for r in matrix) < 3:
        rep.warn("matrix.csv: fewer than 3 applicable rows; check markets and obligations again")
    for rows, col, pat, known, name in ((matrix, "hazards", ID + r"H-\d{2,3}\b", hids, "hazard"),
                                        (hazards, "matrix_refs", ID + r"M-\d{2,3}\b", mids, "matrix")):
        for r in rows:
            bad = [x for x in re.findall(pat, r[col]) if x not in known]
            if bad:
                rep.error(f"{r['id']}: unknown {name} ids {bad}")
    rules = check_arch_rules(os.path.join(reg, "arch-rules.md"), rep, set(mids) | set(hids))
    req_text = "".join(text_of(p) + "\n" for p in sorted(glob.glob(os.path.join(D, "01-requirements", "**", "*"),
                       recursive=True)) if p.lower().endswith((".md", ".yaml", ".yml", ".csv", ".txt")))
    defined, referenced = set(DERIVED.findall(req_text)), {}
    for r in matrix:
        for x in DERIVED.findall(r["derived_reqs"]):
            referenced.setdefault(x, []).append(r["id"])
        if r["applies"].lower() == "yes" and not r["derived_reqs"]:
            rep.warn(f"matrix {r['id']} applies but derives no requirement ('none: <reason>' if so)")
    for r in hazards:
        for x in DERIVED.findall(r["sreq"]):
            referenced.setdefault(x, []).append(r["id"])
    if not defined:
        (rep.error if late else rep.warn)("01-requirements has no REG-/SREQ- requirement yet; traces not checked")
    else:
        for x in sorted(set(referenced) - defined):
            rep.error(f"{x} is named in the register ({', '.join(referenced[x])}) but not in 01-requirements")
        for x in sorted(defined - set(referenced)):
            rep.warn(f"{x} is in 01-requirements but no matrix row or hazard names it (no parent)")
    ncs = load_ncs(reg)
    comp = {row[0].strip(): row[1].strip() for row in csv.reader(io.StringIO(
        text_of(os.path.join(D, "01-requirements", "compliance.csv")))) if len(row) >= 2}
    for nc in ncs:
        nid, sec = nc["id"], nc["sec"]
        if nc["severity"] not in ("major", "minor") or nc["status"] not in NC_STATUS or nc["gate"] not in GATES:
            rep.error(f"{nid}: needs severity major|minor, status {'|'.join(NC_STATUS)}, gate G0-G4")
        if not sec.get("finding") or not sec.get("what closes it"):
            rep.error(f"{nid}: sections '## Finding' and '## What closes it' must not be empty")
        if nc["status"] in ("closure-proposed", "closed") and not sec.get("closure") \
                and "withdrawn" not in sec.get("assessor check", "").lower():
            rep.error(f"{nid}: status {nc['status']} but the Closure section is empty")
        if nc["status"] == "unresolved":
            why = (re.findall(r"lead -> unresolved: (.*)", sec.get("closure", "")) or ["(no reason recorded)"])[-1]
            rep.note(f"{nid} ({nc['severity']}) unresolved, carried as FAIL; lead's reason: {why[:150]}")
            if nc["severity"] == "major" and not re.search(rf"\b{nid}\b", report_text):
                (rep.error if report_text or late else rep.warn)(f"{nid}: unresolved major NC not named in REPORT.md")
            for rid, st in comp.items():
                if st == "met" and re.search(rf"\b{re.escape(rid)}\b", nc["refs"]):
                    rep.error(f"{nid}: unresolved but compliance.csv marks {rid} as met")
    is_open = lambda n, sev: n["severity"] == sev and n["status"] in ("open", "closure-proposed")  # noqa: E731
    open_major = [n["id"] for n in ncs if is_open(n, "major")]
    open_minor = [n["id"] for n in ncs if is_open(n, "minor")]
    unresolved = [n["id"] for n in ncs if n["severity"] == "major" and n["status"] == "unresolved"]
    if gate:
        rounds = gate_rounds(reg, gate)
        if not rounds:
            rep.error(f"gate {gate}: no verdict file REG/gates/{gate}-r<k>.md; the assessor has not ruled")
        else:
            k, verdict = rounds[-1]
            rep.note(f"gate {gate} round {k}: {verdict}; open major {open_major or '-'}; open minor "
                     f"{open_minor or '-'}; unresolved {unresolved or '-'}")
            if verdict is None:
                rep.error(f"gate {gate} round {k}: no 'verdict: PASS|CONDITIONAL|VETO' line")
            elif open_major:
                rep.error(f"gate {gate} is closed: major NC(s) {open_major} open; close them with evidence and ask "
                          f"for a re-check, or switch concept (POLICY 6)")
            elif verdict == "VETO" and not unresolved:
                rep.error(f"gate {gate}: VETO stands until the assessor writes round {k + 1}")
            elif verdict == "VETO":
                rep.warn(f"gate {gate} passed with unresolved major NC(s) {unresolved}, carried as FAIL")
            if verdict == "PASS" and open_minor:
                rep.warn(f"gate {gate}: PASS while minor NC(s) {open_minor} are open")
        if late:
            try:
                res = json.load(open(os.path.join(SCORING, "result.json"), encoding="utf-8"))
                assert res.get("cert_criteria") and res.get("winner")
            except Exception:  # noqa: BLE001
                rep.error("scoring/result.json missing, without winner or without cert criteria (run concept_score.py)")
        if a.tier == "full" and gate in ("G2", "G3", "G4"):
            if not os.path.isfile(os.path.join(D, "02-concept", "architecture.md")):
                rep.error("02-concept/architecture.md missing")
            if not os.path.isfile(TF_INDEX):
                (rep.warn if gate == "G2" else rep.error)("technical-file/index.md missing")
            elif gate == "G4":  # deliverable paths, and other a/b.ext paths outside method/, .claude/, INPUT/
                idx = text_of(TF_INDEX)
                toks = set(DELIV_PATH.findall(idx)) | {t for t in OTHER_PATH.findall(idx)
                                                      if not t.startswith(("method/", ".claude/", "INPUT/"))}
                bases = (D, os.path.dirname(TF_INDEX), os.path.dirname(os.path.dirname(TF_INDEX)))
                for tok in sorted(t.rstrip(".") for t in toks):
                    rel = tok[len(D) + 1:] if tok.startswith(D + "/") else tok
                    if not any(os.path.exists(os.path.join(b, rel)) for b in bases):
                        rep.error(f"technical-file/index.md names a missing path: {tok}")
            if gate == "G4" and not report_text:
                rep.error("REPORT.md missing")
    skip = tuple(os.path.join(reg, s) for s in ("nc", "gates", "sources")) + (os.path.join(D, "02-concept", "benchmark.md"),)
    hits = [f"{os.path.relpath(p, D)}:{n}: {line.strip()[:100]}"
            for p in sorted(glob.glob(os.path.join(D, "**", "*.md"), recursive=True)) if not p.startswith(skip)
            for n, line in enumerate(text_of(p).splitlines(), 1) if is_claim(line)]
    for h in hits[:15]:
        rep.warn(f"possible conformity claim (POLICY 7): {h}")
    rep.note(f"register: {len(matrix)} matrix rows, {len(hazards)} hazards, {len(rules)} rules, {len(ncs)} NCs "
             f"(open major {len(open_major)}, open minor {len(open_minor)}, unresolved {len(unresolved)})")


def cmd_lint(a, rep):
    rows = read_csv(a.file, MATRIX, rep, os.path.basename(a.file))
    if rows is not None:
        lint_matrix(rows, rep, os.path.basename(a.file), need_ids=False)
        rep.note(f"{len(rows)} rows")


def cmd_merge(a, rep):
    path, log_path = os.path.join(a.reg, "matrix.csv"), os.path.join(a.reg, "merge-log.csv")
    os.makedirs(os.path.join(a.reg, "sources"), exist_ok=True)
    rows, chk = [], Report()
    if os.path.isfile(path) and os.path.getsize(path):
        rows = read_csv(path, MATRIX, chk, "matrix.csv")
        if rows is None or chk.errors:
            for e in chk.errors:
                rep.error(e)
            return rep.error("merge refused, nothing written: matrix.csv must parse completely first (exact header; "
                             "quote fields that contain commas). Fix the lines above, then run merge again")
    def key(r):  # one obligation = one jurisdiction + item text (case, spaces and punctuation ignored)
        return (r.get("jurisdiction") or "").strip().upper(), re.sub(r"[^a-z0-9]", "", (r.get("item") or "").lower())

    have = {key(r): r["id"] for r in rows}
    done = {(r.get("source") or "", key(r)): r.get("id") or "" for r in csv.DictReader(io.StringIO(text_of(log_path)))}
    nxt = max([int(x[2:]) for x in [r["id"] for r in rows] + list(done.values()) if re.fullmatch(r"M-\d+", x)],
              default=0) + 1
    new, log = [], []
    for src in sorted(glob.glob(os.path.join(a.reg, "sources", "*.csv"))):
        name = os.path.basename(src)
        srows = read_csv(src, MATRIX, rep, name) or []
        lint_matrix(srows, rep, name, need_ids=False)
        for r in srows:
            k = key(r)
            if (name, k) in done:  # merged before: the lead may have edited or decided that row since
                continue
            if k in have:
                rep.note(f"{name} line {r['_line']}: same item as {have[k]}; not added")
            else:
                have[k], nxt = f"M-{nxt:02d}", nxt + 1
                new.append(dict(r, id=have[k]))
            done[(name, k)] = have[k]
            log.append({"source": name, "line": r["_line"], "jurisdiction": r["jurisdiction"], "item": r["item"],
                        "id": have[k]})
    append_csv(path, MATRIX, new)
    append_csv(log_path, LOG, log)
    first = {}
    for r in rows + new:
        m = re.search(r"\d{2,5}(?:[/-]\d{1,5})+|\b\d{3,6}\b", r["item"])
        if m and r["applies"].lower() != "no":
            first.setdefault(m.group(0), []).append(r["id"])
    for tok, ids in first.items():
        if len(ids) > 1:
            rep.warn(f"possible duplicates (number {tok}): {', '.join(ids)}; keep one, set the others applies=no "
                     f"with why 'duplicate of M-..'")
    rep.note(f"merge: {len(new)} new row(s) appended; matrix.csv has {len(rows) + len(new)} rows")


def cmd_init(a, rep):
    for d in ("sources", "nc", "gates"):
        os.makedirs(os.path.join(a.reg, d), exist_ok=True)
    for d in (os.path.join(D, "01-requirements"), SCORING, os.path.join(D, "02-concept", "calc")):
        os.makedirs(d, exist_ok=True)

    def new(path, content):
        if os.path.exists(path):
            rep.note(f"kept existing {path}")
        else:
            with open(path, "w", encoding="utf-8", newline="") as fh:
                fh.write(content)
            rep.note(f"created {path}")

    new(os.path.join(a.reg, "matrix.csv"), ",".join(MATRIX) + "\n")
    new(os.path.join(a.reg, "hazards.csv"), ",".join(HAZARDS) + "\n")
    new(os.path.join(a.reg, "arch-rules.md"), text_of(os.path.join("method", "templates", "arch-rules.md"))
        or "# Certifiable-architecture rules\n\nFeature tags: \n")
    new(os.path.join(a.reg, "assessor-log.md"), "# Assessor log (written only by the conformity assessor)\n\n")
    new(os.path.join(SCORING, "concepts.csv"), "concept,name,features\n")
    new(os.path.join(SCORING, "criteria.csv"), "criterion,kind,weight,source\ncert-risk,cert,10,POLICY 5\n"
        "cert-cost,cert,8,certification bill from matrix.csv\ncert-time,cert,7,certification bill from matrix.csv\n")
    new(os.path.join(SCORING, "scores.csv"), "concept,criterion,score,low,high,basis\n")
    ids = requirement_ids(os.path.join("INPUT", "requirements.md"))
    if not ids:
        rep.warn("no requirement ids found in INPUT/requirements.md; the compliance.csv skeleton is empty")
    new(os.path.join(D, "01-requirements", "compliance.csv"), "req_id,status,value,unit,evidence,note\n"
        + "".join(f'{i},not_addressed,,,,"pending: work in progress"\n' for i in ids))
    new(os.path.join(D, "manifest.json"), json.dumps({
        "tier": a.tier, "files": [{"path": "01-requirements/compliance.csv", "kind": "requirements",
                                   "desc": "status of every input requirement"}],
        "key_figures": dict.fromkeys(["dimensions_mm", "mass_kg", "power_w", "unit_cost", "retail_price"]),
        "blocked": []}, indent=2) + "\n")
    t0 = datetime.datetime.now().replace(second=0, microsecond=0)
    try:
        if a.start:
            t0 = (t0.replace(hour=int(a.start.split(":")[0]), minute=int(a.start.split(":")[1]))
                  if len(a.start) <= 5 else datetime.datetime.strptime(a.start, "%Y-%m-%d %H:%M"))
    except ValueError:
        rep.warn(f"--start '{a.start}' not understood; using now")
    plan = "\n".join(f"- {(t0 + datetime.timedelta(minutes=m)):%H:%M} (T+{m // 60}:{m % 60:02d}) {t}"
                     for m, t in SCHEDULE[a.tier])
    new("STATE.md", f"# STATE: the lead's working memory (update at every phase boundary)\n\ntier: {a.tier}\n"
        f"T0: {t0:%Y-%m-%d %H:%M} (clock of `date` on this machine)\n\n## Clock plan\n{plan}\n\n## Agents (ids)\n"
        "- researchers:\n- assessor:\n\n## Gates and NCs\n\n## Decisions taken\n\n## Next step\n")


def next_nc(reg):
    nums = [int(m.group(1)) for p in glob.glob(os.path.join(reg, "nc", "NC-*.md"))
            for m in [re.search(r"NC-(\d+)\.md$", p)] if m]
    return f"NC-{max(nums, default=0) + 1:03d}"


def cmd_status(a, rep):
    matrix = read_csv(os.path.join(a.reg, "matrix.csv"), MATRIX, rep, "matrix.csv") or []
    hazards = [r for r in read_csv(os.path.join(a.reg, "hazards.csv"), HAZARDS, rep, "hazards.csv") or []
               if r["risk"].isdigit() and r["res_risk"].isdigit()]
    band = lambda v: "R>=8" if int(v) >= 8 else ("R5-7" if int(v) >= 5 else "R<=4")  # noqa: E731
    count = lambda xs: {k: xs.count(k) for k in sorted(set(xs))}  # noqa: E731
    print(f"matrix: {len(matrix)} rows, applies {count([r['applies'].lower() for r in matrix])}")
    print(f"hazards: {len(hazards)}; initial {count([band(r['risk']) for r in hazards])}; "
          f"residual {count([band(r['res_risk']) for r in hazards])}")
    for nc in load_ncs(a.reg):
        print(f"{nc['id']} {nc['severity']:<5} {nc['gate']} {nc['status']:<16} {nc['title'][:70]}")
    for g in GATES:
        if gate_rounds(a.reg, g):
            print(f"gate {g}: round {gate_rounds(a.reg, g)[-1][0]} verdict {gate_rounds(a.reg, g)[-1][1]}")
    print(f"next NC id: {next_nc(a.reg)}")
    for m in rep.errors:
        print(f"ERROR {m}")


def cmd_nc_status(a, rep):
    path = os.path.join(a.reg, "nc", f"{a.nc}.md")
    text = text_of(path)
    if not text or not re.search(r"(?im)^[\W_]*status\s*:", text):
        return rep.error(f"{path} not found or without a 'status:' line")
    if a.status not in MAY_SET[a.by]:
        return rep.error(f"the {a.by} may set only {MAY_SET[a.by]} (POLICY 6)")
    if not a.note.strip() or (a.status == "closure-proposed" and not evidence_paths(a.note)):
        return rep.error("a note is required; a closure must name an existing evidence file inside deliverables/")
    if a.status == "unresolved":  # the veto holds until the assessor has re-checked the NC and kept it open
        seen = [f for f in glob.glob(os.path.join(a.reg, "gates", "G*-r*.md")) if re.search(
            rf"(?im)^[\W_]*open[ _-]?(?:major|minor)\s*:.*{ID}{re.escape(a.nc)}\b", text_of(f))]
        if len(seen) < 2 and "assessor -> open" not in text:
            return rep.error(f"{a.nc} has not been re-checked: propose its closure or ask for the re-check first; "
                             "unresolved is accepted only after a re-check kept it open (POLICY 6)")
    text = re.sub(r"(?im)^([\W_]*status\s*:\s*).*$", lambda m: m.group(1) + a.status, text, count=1)
    section = "## Closure" if a.by == "lead" else "## Assessor check"
    line = f"- {datetime.datetime.now():%Y-%m-%d %H:%M} {a.by} -> {a.status}: {a.note.strip()}"
    lines = text.rstrip("\n").split("\n")
    low = [x.strip().lower() for x in lines]
    if section.lower() in low:
        i = low.index(section.lower())
        end = next((j for j in range(i + 1, len(lines)) if lines[j].startswith("## ")), len(lines))
        while end > i + 1 and not lines[end - 1].strip():
            end -= 1
        lines.insert(end, line)
    elif a.by == "lead" and "## assessor check" in low:
        i = low.index("## assessor check")
        lines[i:i] = [section, line, ""]
    else:
        lines += ["", section, line]
    with open(path, "w", encoding="utf-8") as fh:
        fh.write("\n".join(lines) + "\n")
    rep.note(f"{a.nc}: status {a.status}")


def cell(s, n=100):
    s = (s or "").replace("|", "/").replace("\n", " ").strip()
    return s if len(s) <= n else s[:n - 1] + "…"


def cmd_render(a, rep):
    matrix = read_csv(os.path.join(a.reg, "matrix.csv"), MATRIX, rep, "matrix.csv") or []
    hazards = read_csv(os.path.join(a.reg, "hazards.csv"), HAZARDS, rep, "hazards.csv") or []
    rules = check_arch_rules(os.path.join(a.reg, "arch-rules.md"), Report(), set())
    ncs = load_ncs(a.reg)
    table = lambda head, rows: [f"| {' | '.join(head)} |", "|" + "---|" * len(head)] + [  # noqa: E731
        f"| {' | '.join(cell(str(c)) for c in r)} |" for r in rows]
    now = lambda g: "; ".join(f"{sum(1 for n in ncs if n['gate'] == g and (n['severity'], n['status']) == s)} {' '.join(s)}"  # noqa: E731
                              for s in sorted({(n["severity"], n["status"]) for n in ncs if n["gate"] == g})) or "no NCs"
    gates = [(g, k, v, now(g) if i == len(rr) - 1 else "") for g in GATES for rr in [gate_rounds(a.reg, g)]
             for i, (k, v) in enumerate(rr)]
    out = [f"# Compliance register (generated by method/gates/register.py render; edit the sources, not this file)",
           "", f"Generated {datetime.datetime.now():%Y-%m-%d %H:%M}. Nothing here is certified: the register identifies "
           "obligations, hazards and routes, and records this run's conformity assessment.", "", "## Gate verdicts", "",
           "The last column shows the NCs raised at that gate as they stand now (an NC raised at G0 can be closed "
           "at a later gate).", ""]
    out += table(["gate", "round", "verdict", "NCs of this gate now"], gates)
    out += ["", "## Nonconformities", ""] + table(["NC", "severity", "gate", "status", "refs", "title", "last closure note"], [
        (n["id"], n["severity"], n["gate"], n["status"], n["refs"], n["title"],
         re.sub(r"^- \S+ \S+ ", "", (n["sec"].get("closure", "").splitlines() or [""])[-1])) for n in ncs])
    out += ["", "## Obligations that apply or may apply", ""] + table(
        ["id", "jur.", "item", "applies", "route", "design-driving clauses", "derived", "cost EUR", "weeks", "source (read)"],
        [(r["id"], r["jurisdiction"], r["item"], r["applies"], r["route"], r["driving_clauses"], r["derived_reqs"],
          r["cost_eur"], r["weeks"], f"{r['source'].split(';')[0]} ({r['read_date']})")
         for r in sorted(matrix, key=lambda r: (r["applies"].lower() != "yes", r["id"]))
         if r["applies"].lower() in ("yes", "maybe")])
    out += ["", "## Not applicable", ""] + table(["id", "item", "why"], [
        (r["id"], r["item"], r["why"]) for r in matrix if r["applies"].lower() == "no"])
    out += ["", "## Hazard register (highest initial risk first)", ""] + table(
        ["id", "phase", "hazard", "situation -> harm", "S x P = R", "measures", "FW-indep.", "residual", "SREQ"],
        [(r["id"], r["phase"], r["hazard"], f"{r['situation']} -> {r['harm']}",
          f"{r['severity']} x {r['probability']} = {r['risk']}", r["measures"], r["fw_independent"],
          f"{r['res_severity']} x {r['res_probability']} = {r['res_risk']}", r["sreq"])
         for r in sorted(hazards, key=lambda r: -int(r["risk"]) if r["risk"].isdigit() else 0)])
    out += ["", "## Certifiable-architecture rules", ""] + table(["id", "rule", "mandatory"], [
        (k, v["title"], "yes" if v["mandatory"] else "no") for k, v in rules.items()])
    dest = a.out or os.path.join(a.reg, "register.md")
    os.makedirs(os.path.dirname(dest) or ".", exist_ok=True)
    with open(dest, "w", encoding="utf-8") as fh:
        fh.write("\n".join(out) + "\n")
    rep.note(f"wrote {dest}")


KINDS = [("01-requirements", "requirements"), ("02-concept/compliance/nc", "nonconformity"),
         ("02-concept/compliance/gates", "gate-verdict"), ("02-concept/compliance/sources", "source-notes"),
         ("02-concept/compliance", "compliance-register"), ("02-concept/scoring", "concept-scoring"),
         ("02-concept/calc", "calculation"), ("02-concept", "concept"), ("03-cad", "cad"), ("04-drawings", "drawing"),
         ("05-calculations", "calculation"), ("06-electronics", "electronics"), ("07-firmware", "firmware"),
         ("08-safety-certification", "safety-certification"), ("09-bom-cost", "bom-cost"),
         ("10-prototype-test-plan", "test-plan"), ("11-renders", "render")]


def cmd_manifest(a, rep):
    path = os.path.join(D, "manifest.json")
    try:
        man = json.load(open(path, encoding="utf-8"))
    except Exception:  # noqa: BLE001
        man = {}
    files = [f for f in man.get("files", []) if isinstance(f, dict) and os.path.isfile(os.path.join(D, f.get("path", "")))]
    listed, added = {f["path"] for f in files}, 0
    for p in sorted(glob.glob(os.path.join(D, "**", "*"), recursive=True)):
        rel = os.path.relpath(p, D)
        if os.path.isfile(p) and rel != "manifest.json" and rel not in listed and "__pycache__" not in rel:
            kind = "report" if rel == "REPORT.md" else next((k for pre, k in KINDS if rel.startswith(pre + "/")), "other")
            files.append({"path": rel, "kind": kind, "desc": f"{kind}: {os.path.basename(rel)}"})
            added += 1
    man.update({"tier": a.tier, "files": files})
    man.setdefault("key_figures", dict.fromkeys(["dimensions_mm", "mass_kg", "power_w", "unit_cost", "retail_price"]))
    man.setdefault("blocked", [])
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(man, fh, indent=2, ensure_ascii=False)
        fh.write("\n")
    rep.note(f"manifest.json: {len(files)} files ({added} added with a generic desc; describe the important ones)")


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--reg", default=os.path.join(D, "02-concept", "compliance"), help="register folder")
    sub = ap.add_subparsers(dest="cmd", required=True)
    for name in ("init", "check", "manifest"):
        sub.add_parser(name).add_argument("--tier", choices=["concept", "full"], required=True)
    sub.choices["init"].add_argument("--start", default="", help="run start HH:MM or 'YYYY-MM-DD HH:MM'")
    sub.choices["check"].add_argument("--gate", choices=GATES)
    sub.add_parser("lint").add_argument("file")
    for name in ("merge", "status", "next-nc"):
        sub.add_parser(name)
    p = sub.add_parser("nc-status")
    p.add_argument("nc")
    p.add_argument("status", choices=NC_STATUS)
    p.add_argument("--by", choices=["lead", "assessor"], required=True)
    p.add_argument("--note", required=True)
    sub.add_parser("render").add_argument("--out", default="")
    a, rep = ap.parse_args(), Report()
    if not (os.path.isdir("INPUT") and os.path.isdir("method")):
        print(f"ERROR run register.py from the run directory: {os.getcwd()} has no INPUT/ and method/")
        return 1
    if a.cmd == "next-nc":
        print(next_nc(a.reg))
        return 0
    if a.cmd == "status":
        cmd_status(a, rep)
        return 0
    {"init": cmd_init, "lint": cmd_lint, "merge": cmd_merge, "check": cmd_check, "nc-status": cmd_nc_status,
     "render": cmd_render, "manifest": cmd_manifest}[a.cmd](a, rep)
    return rep.emit(f"register {a.cmd}")


if __name__ == "__main__":
    sys.exit(main())
files/method/roles/calculations.md (33 wierszy)
# Role: calculations (05-calculations)

Your quality bar is handbook §6.

## Outputs
`deliverables/05-calculations/`:
- One script per question. Each runs as `ddpy <script>` (the toolchain Python, RUN CONTRACT §5) from that
  folder, offline, in at most 5 minutes, and writes `<stem>.results.json`
  (`{"<name>": {"value": x, "unit": "..."}}`). Deliver the results files too.
- `index.md`: for each script, the question, the result, the requirement or SREQ id, and the margin.

The `elec_*.py` scripts belong to the electronics agent, but check their inputs.

## Compliance-first duties
Do the calculations a test lab will ask about first:
- **Temperatures** of accessible surfaces and of rated components, in normal operation and under abnormal or
  single-fault conditions. Compare them with the limits the register cites, and give the source of each
  limit.
- **Insulation coordination**, when the architecture has hazardous voltage: working voltage, pollution
  degree and material group give the creepage and clearance from the cited table. If electronics has already
  done this, check their work.
- **Mechanical and stored energy**, as the hazards require: stability; the strength of parts that carry
  load or protect people; the energy stored in heaters, springs, capacitors or pressure.

Only after these, do the performance calculations behind the given requirements.

## How each script is built
- At the top of the script, label every input with its origin.
- Use units (`pint`) where practical.
- Put a hand estimate in a comment and compare it with the result.
- For a simulation: first run a case with a known answer, then use three mesh or step sizes, then do a
  sanity balance.
files/method/roles/discipline-common.md (55 wierszy)
# Discipline agents: common rules (full tier)

You produce one part of the technical file for a product that is designed compliance-first. The design must
meet the safety and regulatory requirements derived from the compliance register. An independent assessor
reviews your output at gate G3 and can veto it.

## Read first
1. `method/HYGIENE.md` (binding).
2. This file, then your role file, then your brief.
3. The design documents:
   - `deliverables/02-concept/architecture.md`
   - `deliverables/02-concept/concept.md`
   - `deliverables/01-requirements/requirements.md`
4. The register in `deliverables/02-concept/compliance/`: `hazards.csv`, `matrix.csv`, `arch-rules.md`,
   `register.md`. Its format is in `method/templates/register-formats.md`.
5. The handbook section for your discipline in `.claude/skills/dd-hw-handbook/SKILL.md`.

## Rules
- **Ids.** Meet every id your brief lists (INPUT ids, SREQ-, REG-, AR-). In your `design-notes.md`, give
  for each id how it is met, the evidence path, and the status: PASS (analysis), PARTIAL, FAIL or NOT RUN.
- **Write rights.**
  - Write only in the output folders your brief names.
  - Never edit the register, the requirements, the concept, NC files, gate files or REPORT.md.
  - Never create files whose names start with REPORT, SUMMARY, FINDINGS or ANALYSIS, in any letter case:
    the harness refuses them.
- **Problems you find.** You may find a new hazard, an obligation that seems to be missing, or a requirement
  you cannot meet. List it in your final message; the lead updates the register. Do not hide it, and do not
  stop because of it.
- **Tools.**
  - Use only what `INPUT/TOOLCHAIN.md` lists.
  - Prove each tool route on a tiny case with a known answer first (HYGIENE 4).
  - Make three honest attempts, then use a labelled fallback.
- **Numbers.** Quote design numbers from their home (a calculation's result file or architecture.md). A new
  number gets one home and an origin label.
- **Claims.** Never write that anything is certified, approved, listed or compliant. Write what is designed
  and what evidence exists (`method/POLICY.md` §7).
- **Boundaries.**
  - Use the web read-only.
  - Never contact anyone, sign up, buy anything or request quotes.
  - Never fetch, search for or open stragowski.suby.pl.
  - Never read outside the run directory except /usr, /opt, /etc and the toolchain paths.
  - Install nothing.
  - Delete solver scratch you no longer need.
- **Time.** Stop at your time box: partial and honest beats late. The lead may continue you later, with the
  same context, to close a nonconformity or to finish against the final design.

## Final message
Your final message is your report to the lead, at most 25 lines:
- the files you wrote;
- the ids covered, each with its evidence path and status;
- the new numbers and their homes;
- your assumptions;
- new hazards or compliance issues;
- open items.
files/method/roles/electronics.md (39 wierszy)
# Role: electronics (06-electronics)

Your quality bar is handbook §9.

## Outputs
- `deliverables/06-electronics/`:
  - a KiCad 9 project: `.kicad_pro`, `.kicad_sch`, and `.kicad_pcb` when a board is needed;
  - ERC and DRC reports from `kicad-cli`;
  - fabrication outputs: Gerbers, drill files, a position file, and a board BOM with manufacturer part
    numbers;
  - `pinmap.csv` (`signal,mcu_pin,direction,notes`). Write it early, because firmware uses it;
  - `design-notes.md`.
- Circuit calculations as `deliverables/05-calculations/elec_<topic>.py`. Follow the 05 rules: each script
  runs as `ddpy <script>`, offline, and writes `<stem>.results.json`.

## Compliance-first duties
- **Hardware protection.** Build the protection layers of `architecture.md` in hardware. Every SREQ in your
  brief that must act independently of firmware becomes a part on the schematic: a fuse, thermal cut-off,
  interlock, current limit, comparator or hardware watchdog. In `design-notes.md`, name that part next to
  its SREQ.
- **Insulation boundary.**
  - Allow hazardous-voltage nets only where the architecture allows them.
  - Take creepage and clearance values from the standard that the register cites, as a CALCULATION with its
    source.
  - Enforce them as board rules and keep-outs.
- **Pre-certified modules and supplies.** Use them within the conditions of their datasheet or approval
  (antenna keep-out, layout, input range). For each one, write the condition and how you met it.
- **EMC by design.**
  - input filtering;
  - return paths;
  - control of edge rates;
  - ESD and transient protection at external connectors.

  List these measures for the pre-compliance plan.

## Checks
- ERC and DRC are clean, or every waiver is justified.
- The power budget and the dissipation of hot parts are calculated.
files/method/roles/firmware.md (29 wierszy)
# Role: firmware (07-firmware)

Your quality bar is handbook §10.

## Outputs
`deliverables/07-firmware/`:
- a PlatformIO project (`platformio.ini` at the folder root) that builds offline for the MCU on the schematic;
- a `native` test environment with host tests;
- `design-notes.md`;
- `logs/`: the logs of `pio run` and `pio test -e native` (the commands are in `INPUT/TOOLCHAIN.md`).

## Compliance-first duties
- **States.** Use a state machine with explicit fault states and a safe default state, a watchdog, and
  debounced inputs. Go to the safe state on brown-out, on loss of the control signal, and on sensor failure.
- **Two layers of protection.** For every SREQ in your brief, name two things in `design-notes.md`:
  - the hardware backstop that acts if the firmware fails (from `architecture.md` or the schematic);
  - the firmware's own second layer.

  The firmware is never the only protection.
- **Host tests.** Write a host test for every interlock and every fault reaction, so that it fails first and
  then passes. Build with warnings as errors where the framework allows it.
- **Pins.** Take pins from `06-electronics/pinmap.csv` when it exists, otherwise from `architecture.md`.
  Record which source you used.
- **Safety-relevant software.** If the register cites a software clause of a standard, write in
  `design-notes.md` how the design meets it: simple, testable, no single point of failure.

## Never
Never flash or power anything.
files/method/roles/mechanical.md (37 wierszy)
# Role: mechanical design (03-cad, 04-drawings)

Your quality bar is handbook §7 and §8.

## Outputs
- `deliverables/03-cad/`:
  - one parametric script per designed part, plus an assembly script that places the named parts;
  - one STEP file (AP214 or AP242) per designed part, and `assembly.step`;
  - `parts.csv` (`part,file,material,density_kg_m3,qty`);
  - `design-notes.md`: the parameters, the process per part, and how each id in your brief is met (with
    paths);
  - `checks/`: the check outputs listed below.
- `deliverables/04-drawings/`: a drawing of every manufactured part, and an assembly drawing whose balloons
  match the item numbers in `bom.csv` (PDF or SVG).

## Compliance-first duties
- **The enclosure is part of the protection.** Show which parts act as the electrical, fire and mechanical
  enclosure that the cited standard expects: openings, access to hazardous parts, hot surfaces and their
  guards, stability. The insulation boundary, creepage paths and zones of `architecture.md` must exist in
  the geometry.
- **Materials.** Give each part a material with the flammability and temperature rating the register asks
  for, and the source of that rating.
- **Labels.** Provide space and a position for the rating plate, the warning labels and the markings your
  brief lists.
- **Process.** Choose a process for each part for the prototype and for 1000 units; wall thickness and draft
  must suit the process.

## Checks
Write their outputs to `03-cad/checks/`:
- open every STEP with the toolchain's STEP checker (valid solid, volume, bounding box);
- check the assembly for interference;
- compute mass from density;
- compare the bounding box with the size requirements.

## Not your job
Do not write to `11-renders/`; the renders agent converts your `assembly.step`.
files/method/roles/renders.md (21 wierszy)
# Role: renders (11-renders)

Your quality bar is handbook §14.

## Outputs
`deliverables/11-renders/`:
- `assembly.glb`: convert `03-cad/assembly.step` with the toolchain's STEP→GLB converter, then check it with
  the glTF validator.
- PNG renders, made with Blender in background mode from that GLB:
  - a product view;
  - an exploded view;
  - a view that shows the user interface and where the rating plate and labels sit.
- `render-notes.md`: the scene script, the camera, a scale check against a known dimension, and what each
  render shows.

## Rules
- Never use generative image models.
- Check what can be measured instead of trusting a glance: the file exists, the resolution is right, the
  object fills the frame, the named parts are present.
- Label every image as a render of a digital design.
files/method/roles/sourcing.md (39 wierszy)
# Role: sourcing, BOM and cost (09-bom-cost)

Your quality bar is handbook §12.

## Outputs
`deliverables/09-bom-cost/`, in the formats the RUN CONTRACT fixes (the `Formats:` line of your brief is
copied from it; if the two ever differ, the brief wins):
- `bom.csv` with exactly this header:
  `item,qty,description,manufacturer,mpn,supplier,supplier_part,url,unit_price_1,unit_price_100,unit_price_1000,currency,notes`
  Real purchasable parts only. A price read from a supplier page carries its date in `notes`; any other price
  is an ASSUMPTION.
- `cost.json`, exactly this shape:
  `{"currency": "EUR", "bom": {"q1": x, "q100": x, "q1000": x}, "other": {"<name>": {"q1": x, "q100": x, "q1000": x}}, "unit_cost": {"q1": x, "q100": x, "q1000": x}}`
  `bom` is the sum of qty × unit price in `bom.csv`, and `unit_cost` is `bom` plus every `other` line, at
  each quantity.
- `cost.md`: the roll-up at 1, 100 and 1000 units, and the retail-price logic.

## Where to look
Use the offline parts catalogue described in `INPUT/TOOLCHAIN.md` first. Then use distributor pages,
read-only.

## Compliance-first duties
- **Certified sub-assemblies.** Prefer catalogued, certified sub-assemblies: external supplies, power
  modules, radio modules, mains switches, cords, connectors, thermal cut-offs, fuses. Put the approval
  evidence you found in `notes`: the certificate, listing or grant number, the URL, and the date you read it.
  For a part whose approval you cannot find, write "approval not found". Never assume an approval.
- **Insulating materials.** For plastics and insulating materials, give the ratings the design files name
  (flammability class, temperature index) and the URL of the datasheet.
- **Substances.** Give the RoHS/REACH status from the manufacturer's page where one exists.
- **Costs beyond the BOM.** In `cost.json`, `other` includes:
  - `certification`: the one-off certification costs of the chosen concept, from
    `deliverables/02-concept/scoring/result.json` (`bills`, the winner) and the matrix rows. Amortise them
    over 1, 100 and 1000 units and state the assumption;
  - tooling;
  - assembly and test;
  - packaging;
  - freight;
  - contingency.
files/method/roles/test-plan.md (25 wierszy)
# Role: prototype and test plan (10-prototype-test-plan)

Your quality bar is handbook §13.

## Outputs
`deliverables/10-prototype-test-plan/`:
- `build-plan.md`: how each part is made, the order of assembly, and the checks after each step.
- `bring-up.md`: the first power-up with a current-limited or isolated supply, the measurements to take
  before full power, and the safety precautions.
- `tests.csv`, with the header `test_id,closes,method,equipment,conditions,pass_criterion,stage`. `stage`
  is one of `prototype`, `pre-compliance`, `accredited-lab`, `production`.
- `tests.md`: the same tests, in detail.

## Compliance-first duties
- **Coverage.** Write at least one test per INPUT requirement id and per SREQ- and REG- id, listed in the
  `closes` column. None may be missing: check this with a small script that compares the ids, and keep that
  script and its output in the folder.
- **Pre-compliance tests**, where they apply: EMC scans, temperature rise, abnormal operation, leakage and
  insulation.
- **Lab tests.** List the tests an accredited lab will run for each matrix row whose route involves a lab or
  a notified body.
- **Production tests.** Include the routine tests the applicable standard requires for every unit, and the
  end-of-line test.
- **Life tests.** Add durability or life tests wherever lifetime is a requirement.
files/method/templates/arch-rules.md (15 wierszy)
# Certifiable-architecture rules

These are the rules that every concept, and later the design, must satisfy so that conformity is easy to show.
Each rule comes from the compliance matrix (M- ids) and the hazard register (H- ids). The defaults are in
`method/POLICY.md` §2. Replace the example section below with real rules and keep this format; the format is
specified in `method/templates/register-formats.md`.

Feature tags: <tag> (meaning), <tag> (meaning), …

## AR-01 — <the rule in one sentence>
- basis: <M-nn; H-nn>
- mandatory: <yes|no>
- check: <how to tell from a concept description or a design file whether the rule holds>
- cost of violating: <the extra certification work, cost and time a violation triggers>
files/method/templates/brief.md (33 wierszy)
# Subagent brief skeleton

Copy this skeleton into the `prompt` of an `Agent` call. Fill every <…> and delete the lines that do not
apply.

```
You are <role>. Read method/HYGIENE.md first, then <your agent definition | method/roles/discipline-common.md and method/roles/<role>.md>.
Run directory: <absolute path>. Work only inside it; paths below are relative to it.
Product: <2-4 lines from INPUT/brief.md: what it is, who uses it, target markets, power source, main functions>.
Feature tags: <tags and meanings from deliverables/02-concept/compliance/arch-rules.md>.
Task: <one research question | one deliverable>.
Inputs: <paths>.
Must satisfy / cover: <INPUT ids, SREQ- ids, REG- ids, AR- ids>.
Outputs (write nothing else): <exact paths>.
Formats (from RUN CONTRACT §5, word for word, for each output folder; you never see the RUN CONTRACT): <text>.
Time box: it is now <HH:MM>; finish by <HH:MM>. At the box, write what you have and end.
Boundaries: read-only web; never contact anyone, sign up, buy anything or request quotes; never fetch, search for or open stragowski.suby.pl (any path, copy, cache or archive); never read outside the run directory except /usr, /opt, /etc and the toolchain paths; install nothing; never create a file whose name starts with REPORT, SUMMARY, FINDINGS or ANALYSIS (any letter case).
End: your final message is your report, in the format of your role file.
```

## Gate request to the assessor

Send it in the `prompt` of the first `Agent` call, then as `SendMessage` messages.

```
GATE G<n> round <k> — <envelope | concept freeze | architecture | detailed design | release>. Tier: <tier>.
Run directory: <absolute path>. It is now <HH:MM>; finish by <HH:MM>.
Leading concept: <id and name; see deliverables/02-concept/scoring/result.md>.
Changed since your last gate: <paths>.
Closures proposed: <NC ids, or none>.
Follow your gate questions. Write the NCs and REG/gates/G<n>-r<k>.md, then end with the VERDICT line.
```
files/method/templates/register-formats.md (144 wierszy)
# Compliance register: file formats

The register is the single source of truth for this run's compliance work. It lives in
`deliverables/02-concept/compliance/` (called REG below) at both tiers. `python3 method/gates/register.py`
creates it (`init`), validates it (`lint`, `check`) and renders it as tables (`render`). Anyone who cites a
legal item, hazard, rule or requirement cites it by id.

CSV rules: UTF-8; the header exactly as shown; one record per line; inside a field, separate list items with
`;`; put a field in double quotes if it contains a comma or a double quote; dates as `YYYY-MM-DD`; money in
EUR unless `method/POLICY.md` §4 says otherwise.

Ids: `M-nn` matrix row · `H-nn` hazard · `AR-nn` architecture rule · `REG-nn` regulatory requirement (its
parent is a matrix row) · `SREQ-nn` safety requirement (its parent is a hazard) · `DR-<AREA>-nn` any other
derived requirement (handbook §2) · `NC-nnn` nonconformity · `G0`–`G4` gate. Never renumber or reuse an id.
A withdrawn item keeps its id and gets a note.

## REG/matrix.csv: one row per legal act, standard, rule, listing or obligation

`id,jurisdiction,item,applies,why,trigger,driving_clauses,hazards,derived_reqs,evidence_needed,route,cost_eur,weeks,source,read_date`

- `id`: `M-01`, `M-02` … (`register.py merge` gives researcher rows their ids).
- `jurisdiction`: `EU`, `US`, `CA`, `UK`, `INT` (an international standard) or `other`.
- `item`: the number and title, for example a directive number with its name, or a standard with its edition year.
- `applies`: `yes`, `no` or `maybe`. `why`: one line naming the scope clause or product feature that decides it.
- `trigger`: `always`, or one or more feature tags (separated by `;`). The row applies to a concept that has
  any of those tags. Tags are defined at the top of `arch-rules.md`.
- `driving_clauses`: the clause numbers or topics that shape the design. Only use clause numbers that a source
  you read shows.
- `hazards`: the `H-` ids this row addresses. `derived_reqs`: the `REG-`/`SREQ-` ids derived from it, or
  `none: <reason>`.
- `evidence_needed`: what the technical file must contain (calculation, lab test report, marking, declaration …).
- `route`: the conformity route, for example self-declaration with internal production control, notified
  body, supplier's declaration, certification body, safety listing by a recognised lab, registration, `n/a`.
- `cost_eur`, `weeks`: rough one-off cost and duration of this item, as a number or a range `a-b`. They are
  ASSUMPTION unless the source shows a price.
- `source`: the URL(s) you read (separated by `;`). `read_date`: when you read them. Rows marked `yes` or
  `maybe` must have both.

`register.py merge` appends the researcher rows it has not merged before, with new ids, and records which
source row became which id in `REG/merge-log.csv` (do not edit it). It never rewrites a row, so the lead edits
`matrix.csv` freely; it refuses to run while `matrix.csv` does not parse.

## REG/hazards.csv: hazard register in the ISO 12100 manner

`id,phase,hazard,situation,harm,severity,probability,risk,measures,fw_independent,res_severity,res_probability,res_risk,sreq,matrix_refs`

- `id`: `H-01` … `phase`: one of transport, installation, use, cleaning, maintenance, misuse, failure, disposal.
- `hazard`: type and source (electrical, thermal, fire, mechanical, chemical or substance, radiation, noise,
  ergonomic, a combination). `situation`: who is exposed, when and how. `harm`: the injury or damage.
- `severity` and `probability`: integers 1–4 on the scale in `method/POLICY.md` §3. `risk` = severity × probability.
- `measures`: the risk reductions in the 3-step order, each with a prefix: `I:` for inherently safe design,
  `P:` for a protective device or safeguard, `U:` for information for use. Separate them with `;`.
- `fw_independent`: `yes` when a measure that does not depend on firmware covers the hazard; `n/a` when an
  `I:` measure removes the hazard; otherwise `no`.
- `res_severity`, `res_probability`, `res_risk`: the same values after the measures (`res_risk` is their product).
- `sreq`: the `SREQ-` ids that carry the measures into the requirements. `matrix_refs`: the `M-` ids
  (standards and clauses) that apply.

## REG/arch-rules.md: certifiable-architecture rules

Begin with a line `Feature tags: <tag>, <tag>, …` that lists the tags used in `matrix.csv` and
`scoring/concepts.csv`, each tag with a few words of meaning. Then one section per rule:

```
## AR-01 — <the rule in one sentence>
- basis: M-03; H-02
- mandatory: yes
- check: <how to tell from a concept description or a design file whether the rule holds>
- cost of violating: <the extra certification work, cost and time that a violation triggers>
```

A concept that breaks a rule marked `mandatory: yes` is knocked out (it becomes a `ko` criterion in scoring).

## REG/nc/NC-nnn.md: nonconformity (only the assessor creates these)

```
# NC-001 — <one-line title>
severity: major
gate: G1
status: open
refs: M-04; H-02; AR-03; <requirement ids>
raised: YYYY-MM-DD HH:MM

## Finding
<what is wrong, citing the clause, rule, hazard or requirement>

## Evidence
<file paths and short quotes that show it>

## What closes it
<the objective evidence that would close it>

## Closure

## Assessor check
```

`status` is one of `open`, `closure-proposed`, `closed`, `unresolved`. Change it only with
`register.py nc-status`, which also appends a dated note to the right section. The lead can set
`closure-proposed` (the note must name existing evidence files inside `deliverables/`) or `unresolved` (with
the reason, only after a re-check has kept the NC open; `method/POLICY.md` §6). The assessor can set `closed`
or `open`. Nobody edits the text above `## Closure` after the NC is raised.

## REG/gates/G<n>-r<k>.md: gate verdict (only the assessor writes these; round k = 1, 2, 3)

```
# G1 round 1 — <gate name>
verdict: VETO
date: YYYY-MM-DD HH:MM
reviewed: <files read>
open_major: NC-003; NC-004
open_minor: NC-005

## Basis
<why this verdict: what is good enough and what is not>

## Sources spot-checked
<URL: matches / does not match / could not open>

## Before the next gate
<what must be true at the next gate>
```

A verdict is `VETO` when any major NC is open, `CONDITIONAL` when only minors are open, and `PASS` when nothing
is open. Gates: G0 regulatory envelope · G1 concept freeze · G2 architecture · G3 detailed design · G4 release.
`REG/assessor-log.md` is the assessor's own memory (one entry per gate). `REG/register.md` is generated by
`register.py render`. Do not edit it by hand.

## deliverables/02-concept/scoring/: concept selection (`python3 method/gates/concept_score.py`)

- `concepts.csv`: `concept,name,features`. Use `A`, `B`, `C` for `concept`. `features` holds the feature tags
  (separated by `;`) that select the triggered matrix rows for that concept.
- `criteria.csv`: `criterion,kind,weight,source`. `kind` is one of:
  - `req`: weight from the requirement priorities; `source` lists the requirement ids.
  - `cert`: `cert-risk`, `cert-cost`, `cert-time`, created by `init`.
  - `ko`: a knock-out (a mandatory `AR-` id, or a requirement threshold); leave `weight` empty.
- `scores.csv`: `concept,criterion,score,low,high,basis`.
  - `score`: 1–5 (5 is best) for `req` and `cert-risk`; 1 = pass, 0 = fail for `ko`.
  - `low` and `high`: an optional range for an uncertain score.
  - `basis`: the number behind the score and its origin (a CALCULATION path, a SOURCE or an ASSUMPTION).
  - Do not score `cert-cost` or `cert-time`: the script computes both from the certification bill (the
    matrix rows each concept triggers), on a ratio scale.
- `result.json` and `result.md` are written by the script. Never edit them.
files/method/templates/technical-file-index.md (27 wierszy)
# Technical file — index

Path: `deliverables/08-safety-certification/technical-file/index.md`. The lead owns this file and keeps the
status column current; it doubles as the progress tracker of the full tier.

Paths are relative to `deliverables/`. Status words: PASS (analysis), PARTIAL, FAIL, BLOCKED, NOT RUN.

Nothing in this file is certified. It prepares the conformity assessment by an accredited lab or a notified
body, and the manufacturer's declarations.

The compliance register (the data) lives in `02-concept/compliance/` and is the single source.
`08-safety-certification/technical-file/register.md` is generated from it by the register tool (`render --out`).
Gate G4 checks that every path named here exists, so name only paths and keep standard numbers in TF-4's file.

| § | Section | Content (paths) | Owner | Register ids evidenced | Status |
|---|---|---|---|---|---|
| TF-1 | Product description: intended use, foreseeable misuse, variants, markets, photos/renders | 08-safety-certification/technical-file/TF-1-description.md; 11-renders/ | lead | | |
| TF-2 | Design and manufacturing information: CAD, drawings, schematics, board, firmware, parts list | 03-cad/; 04-drawings/; 06-electronics/; 07-firmware/; 09-bom-cost/bom.csv | disciplines | | |
| TF-3 | Risk assessment in the ISO 12100 manner: method, hazard register, residual risks | 08-safety-certification/technical-file/TF-3-risk-assessment.md; 02-concept/compliance/hazards.csv | lead | | |
| TF-4 | Legislation and standards applied, with clause-by-clause status of the design-driving clauses | 08-safety-certification/technical-file/TF-4-standards.md; 02-concept/compliance/matrix.csv | lead | | |
| TF-5 | Calculations and design verification | 05-calculations/ | calculations | | |
| TF-6 | Tests: prototype, pre-compliance, accredited lab, production routine tests | 10-prototype-test-plan/ | test plan | | |
| TF-7 | Draft declarations (EU declaration of conformity; the US declaration or certification route) | 08-safety-certification/technical-file/TF-7-declarations-draft.md | lead | | |
| TF-8 | User information, warnings, markings, rating plate, packaging marks | 08-safety-certification/technical-file/TF-8-user-information.md | lead | | |
| TF-9 | Conformity assessment record of this run: gates, NCs, verdicts | 08-safety-certification/technical-file/register.md; 02-concept/compliance/gates/ | assessor / lead | | |
| TF-10 | Route per market: lab and notified body, pre-compliance plan, cost, time, human-only steps | 08-safety-certification/technical-file/TF-10-route.md | lead | | |
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.