Wariant v007
{'name': 'CONTRACT', 'D1': 'architect (top session) + parallel subsystem owners', 'D2': 'versioned contracts registry; amendments routed to the affected owners (PR with required reviewers)', 'D3': 'interface-first (DSM ordering), then parallel', 'D4': 'by subsystem (not by discipline)', 'D5': 'executable contract checks (budgets, units, allocation, conformance)', 'D6': 'architect for partition; affected owners accept/reject amendments; budget margins decide architecture', 'D7': 'Agent-tool subagents (+ SendMessage)', 'D8': '2-3 candidate partitions, 2 principles per subsystem'}
- Rodzic
- brak (osobnik startowy)
- Operator
- scratch
- Utworzony
- 30 października 2026, 03:55
Co się zmieniło
CONTRACT (W1 paradigm P06): interface-first, contract-driven parallel design. The top session is the architect: it writes the requirements, a functional decomposition and 2-3 candidate partitions of the product into physical subsystems, compares them by coupling (a design structure matrix) and budget closure, and freezes a machine-checked contract registry for the winner (interfaces.yaml with envelopes, interface parameters, rules and pin maps; budgets.yaml with limits, reserves and allocations; allocation.yaml mapping every requirement id; dsm.csv; VERSION). One subsystem-owner subagent (opus) per subsystem then designs its whole subsystem across disciplines in parallel against the frozen contract: two working principles, numbers, real parts, a conformance file with every obligation, budget actual and requirement status. Contract changes are versioned amendments routed by the tool to exactly the affected owners, who accept or reject by SendMessage; contract_check.py must pass at every freeze and at release. System alternatives are combinations of the owners' principles, evaluated by budget closure, a weighted score and a sensitivity check. At the full tier owners detail their subsystems (CAD sub-assemblies in the product frame, KiCad projects, firmware, calculations, drawings) and integration_check.py verifies CAD bounding boxes against envelopes, netlists against pin maps and BOM sums against budgets.
Wyniki
Ten wariant nie ma jeszcze przebiegów.
Pełna treść wariantu
MAIN.md (289 wierszy)
# CONTRACT method: interfaces and budgets first, then subsystem owners in parallel
## 1. The method
You are the **architect** (the top session). Before any detail exists you cut the product into 2-5 physical
subsystems and freeze a **machine-checked contract** between them (envelopes, interface parameters with bounds
and units, pin maps, budgets that must close with a reserve, the allocation of every requirement id). Then one
**owner** per subsystem (a `subsystem-owner` subagent) designs that whole subsystem across disciplines, in
parallel, against the contract; it changes only through amendments the affected owners accept or reject.
Why: most integration failures are interface failures; an executable contract (`method/gates/contract_check.py`
must pass at every freeze) gives parallelism with little chatter, and budgets that must close expose an
infeasible concept before detail work is spent on it.
## 2. First steps
1. Run `date`. Create `state/ARCH-STATE.md` with the start time T, the tier and the clock time of every
checkpoint of section 5 (concept tier) or 6 (full tier). Update it at every checkpoint: phase, decisions,
owners table, open amendments, next step - so you or a restarted session can resume from files alone.
2. Read `INPUT/brief.md`, `INPUT/requirements.md`, `INPUT/TOOLCHAIN.md`, `method/HYGIENE.md` (binding for you and
for every agent you start; every brief tells them to read it), `method/POLICY.md` (the decision rules that
replace the human) and the handbook `.claude/skills/dd-hw-handbook/SKILL.md` (read the file; it is the
quality bar of every deliverable). Skim the formats in `method/templates/`.
3. Create the folders:
```
mkdir -p state work deliverables/01-requirements deliverables/02-concept/calc deliverables/02-concept/contracts/candidates deliverables/02-concept/contracts/amendments deliverables/02-concept/subsystems/SYS
```
4. Launch the benchmark helper (4.4) at once, then start section 5, step A.
## 3. Team, files and write rights
| who | does | writes |
|---|---|---|
| you, the architect | requirements, decomposition, partitions, the contract, amendment routing, integration, every system-level document, REPORT.md, manifest | everything not listed below; the registry only through the tool (4.1) |
| owner of S (`subsystem-owner` subagent, one per subsystem, all in parallel) | subsystem S end to end: two principles, numbers, real parts, cost, hazards; full tier also its CAD, drawings, calculations, electronics, firmware if it owns it | `deliverables/02-concept/subsystems/<S>/`, `work/<S>/`, new amendment files `AMD-<S>-<nn>.yaml`; full tier also `03-cad/<S>/`, `04-drawings/<S>/`, `05-calculations/<S>-*.py`, `06-electronics/<S>/`, `07-firmware/` (only the `owns_firmware` owner) |
| benchmark helper (general-purpose subagent, once) | comparable products | `deliverables/02-concept/benchmark.md` |
- Below, `contracts/` means `deliverables/02-concept/contracts/` and `subsystems/` means
`deliverables/02-concept/subsystems/`. Both live inside `deliverables/`, so the package is complete at every
moment. Write full paths in every brief.
- The registry (`contracts/interfaces.yaml`, `budgets.yaml`, `allocation.yaml`) is yours alone and changes only
through `--freeze` (before the owners start) or `--merge` (after). The tool writes `VERSION`, `history/`,
`dsm.csv`, `REGISTRY.md`, `architecture.mmd/.png` and `CHANGELOG.md`.
- Owners never edit the contract or another subsystem; you never edit an owner's files, except when you take
over a failed subsystem (section 8).
- `SYS` is your own pseudo-subsystem for system-level ids (integration, compliance route, documentation,
packaging, assembly and test labour, retail price). Write `subsystems/SYS/conformance.yaml` with
`subsystem: SYS`, `requirements:` entries and, optionally, `budgets:` actuals of SYS's own allocations; a SYS
entry overrides everything else for that id in compliance.csv.
- Subagents cannot create files named `REPORT*`, `SUMMARY*`, `FINDINGS*` or `ANALYSIS*`: only you write
`deliverables/REPORT.md`.
## 4. Tools and communication
### 4.1 The contract tool (run from the run directory)
| command (`ddpy method/gates/contract_check.py ...`) | use |
|---|---|
| `--ids > state/ids.yaml` | allocation skeleton of every requirement id, with titles and priorities |
| `--candidates` | compare candidate partitions; writes `contracts/candidates/metrics.md` with a suggested pick |
| (no mode) / `--release` / `--json` | check registry, conformance, budget roll-up, amendments; `--release` is strict |
| `--freeze "note"` | freeze contract v1 (green check required; refused once owners conform) |
| `--brief <S>` | an owner's contract view and conformance skeleton |
| `--route AMD-x`, `--vote AMD-x <S> ACCEPT\|REJECT "why"`, `--merge AMD-x`, `--reject AMD-x "why"` | the amendment protocol (4.3) |
| `--alternatives` | budget closure, weighted score and sensitivity of `contracts/alternatives.yaml`; writes `alternatives.md` |
| `--compliance deliverables/01-requirements/compliance.csv` | compliance.csv: a SYS entry wins; an id whose allocation names a `budget:` gets the system roll-up (value, status vs limit and goal); otherwise the owners' worst status |
Owners run `--brief <S>` and the self-check `--subsystem <S>`. Full tier: `ddpy method/gates/integration_check.py
[--subsystem S]` (CAD vs envelopes, assembly interference, netlists vs pin maps, BOM vs budgets). Package:
`python3 method/gates/dd_check.py --tier <tier> --run-scripts`. Run the gate scripts; do not read their source
(formats are in `method/templates/`, problems are named in their ERROR lines) unless a traceback forces you to.
### 4.2 Owners: launch, messages, resume
- Launch all owners in ONE message so they run in parallel, one Agent call each, the prompt filled from
`method/templates/owner-tasks.md`:
`Agent({description: "owner S2-control CONCEPT", subagent_type: "subsystem-owner", prompt: "<filled prompt>"})`.
The call returns at once with the agent's id: record it in the owners table of ARCH-STATE.md (subsystem,
agent id, pass, deadline, status). Its final message (the OWNER REPORT) arrives later as a notification.
- Messages: load SendMessage once with `ToolSearch({query: "select:SendMessage", max_results: 1})`, then
`SendMessage({to: "<owner agent id>", summary: "<5 words>", message: "<first line says what it is>"})`.
Owners reach you with `to: "main"`. Messages and notifications arrive by themselves between tool calls:
never poll for them.
- The clock never wakes you; only notifications and messages do. Owners stop by the end time in their task
without being told (TIME is a backup). So keep working on step D until the TIME checkpoint; if you run out
of work before it, make ONE foreground Bash wait until then
(`until [ "$(date +%s)" -ge "$(date -d '<HH:MM>' +%s)" ]; do sleep 15; done`, Bash timeout just above the
wait, at most 600000; more than 10 minutes early: deepen step D first), never a chain of waits. After TIME,
end your turn with a one-line status: the owners' final reports wake you.
- A finished owner resumes with its context when you SendMessage it: use that for votes, FIX, SWITCH and
re-conformance. Launch a fresh owner only for a new full-tier pass or after a failure.
- Message kinds (the first word says which). Owner to you: `AMD-<id> proposed: ...`, `AMD-<id> ACCEPT: ...`,
`AMD-<id> REJECT: ...`, `OWNER REPORT ...`. You to an owner: the vote request printed by `--route`,
`CONTRACT v<N> in force: ...`, `TIME: ...`, `FIX: ...`, `SWITCH: ...` (texts in owner-tasks.md). Owners never
message each other: the contract is their interface.
### 4.3 Amendments: the only way the contract changes after v1
1. An owner who cannot meet a contract value, or needs something the contract lacks, writes
`contracts/amendments/AMD-<S>-<nn>.yaml` (`method/templates/amendment.yaml`) and messages you.
2. `--route AMD-x` applies the change in memory and runs the contract-level check. Errors: send them to the
proposer, who revises the same file (still `proposed`) and messages you again; or `--reject`. Otherwise it
records the affected owners and prints the vote request: send it to each voter.
3. Record each answer with `--vote`. One REJECT rejects the amendment: forward the reason to the proposer, who
redesigns inside the current contract or proposes something else.
4. Every voter accepted: `--merge AMD-x` (applies, re-checks, freezes the next version), then send
`CONTRACT v<N> in force: ...` to every affected owner.
5. You never decide an amendment alone, with one exception: when the check reports a `SYSTEM BUDGET
VIOLATION`, resolve it in the order of POLICY section 2. The reserve (step 1) or a derived limit (step 4):
an `AMD-SYS-<nn>` with `basis: system_budget_violation` that you `--merge` directly (the tool refuses it if
it lowers an owner's allocation or changes anything but the violated budgets), then notify the affected
owners. Allocation taken from another owner (step 2) is routed like any amendment; a principle switch
(step 3) is a SWITCH message. Your other changes (for example the full-tier detail contract) are
`AMD-SYS-<nn>` amendments routed like any other.
6. At most two rounds per issue. Still unresolved: the current contract stays, the owner reports the
non-conformance honestly, and it goes to REPORT.md as an open risk.
### 4.4 Benchmark helper (once, at the start)
`Agent({description: "benchmark research", subagent_type: "general-purpose", model: "sonnet", prompt: "Run
directory <absolute path>. Read method/HYGIENE.md, section 3 of .claude/skills/dd-hw-handbook/SKILL.md,
INPUT/brief.md and INPUT/requirements.md. Load WebSearch and WebFetch first with ToolSearch
select:WebSearch,WebFetch. Find at least five comparable products (commercial and open source; adjacent categories
count): the specifications that matter for these requirements, price with date and URL, weak points from reviews,
teardowns and forums. Read pages, never guess; report failed lookups. Write deliverables/02-concept/benchmark.md:
the table, realistic target values, the differentiator, the top user complaints. Time box 20 minutes. End with a
10-line summary of targets, differentiator and complaints."})`
## 5. Concept tier: work order (target 60 minutes, hard stop 2 hours)
Times are minutes after T.
**A. T+0-10 Requirement table and decomposition.**
`ddpy method/gates/contract_check.py --ids > state/ids.yaml`. Write `deliverables/01-requirements/requirements.md`
v1 as a table only: id, title, threshold and target with unit, priority, allocated to (filled after C).
Interpretation, design consequence and derived requirements follow in step D. Functions and flows (verb-noun
functions; energy, material and signal flows) and a short physics map (handbook section 4) in
`deliverables/02-concept/physics-map.md`. Choose the budgets (POLICY section 3).
**B. T+10-17 Candidate partitions, at sketch depth.**
Write 2-3 files `contracts/candidates/P<n>.yaml` (`method/templates/candidate.yaml`, about 3 minutes each)
that cut the product differently (POLICY section 2: physical modules, never a split by discipline): subsystems
with a one-line scope, interfaces as id, kind, provider and consumers (no params), one rough estimate per budget
and subsystem, allocation by patterns. Run `--candidates`. Write `deliverables/02-concept/partitions.md`: each
candidate and its rationale, the metrics table, the decision and why each loser lost.
**C. T+17-25 Contract v1.**
Write `contracts/interfaces.yaml`, `budgets.yaml` and `allocation.yaml` from the winner (formats in
`method/templates/`): the product frame and envelope; envelope boxes; every interface with the params that
couple the subsystems at concept depth (levels, flows, loads, heat, mounting; rule, bounds, unit, party,
origin; connector parts and pin maps come with the detail contract, F1); budgets with limit, goal, reserve and
allocations (each allocation at least the estimate); EVERY requirement id allocated (exact ids for priority 4
and 5; `budget:` on ids a budget decides); `owns_firmware` on one subsystem; two `principles` hints per
subsystem. Order: fix the parameters of the most depended-on interfaces first (providers before consumers) and
give every coupled block its shared parameters explicitly, so no owner ever waits for another. Run the check
until green, then `--freeze "v1: <partition>"`.
**D. T+25 Launch the owners** (CONCEPT prompt; wrap-up at T+43, hard end T+47). While they work: complete
`01-requirements/requirements.md` (interpretation and design consequence per given id; derived requirements
`DR-<AREA>-<nn>` for every area of handbook section 2; conflicts and decisions; assumptions register; the
allocation column); fold the benchmark summary into targets and derived requirements; draft concept.md
sections 1, 2, 7 and 8; write `subsystems/SYS/conformance.yaml`; route amendments as they arrive. At T+43 send
TIME to every owner still running (4.2: work until then, at most one bounded wait).
**E. T+43-51 Integration.**
Run the check. An owner's errors: FIX message. Amendments: 4.3. System budget violation: 4.3 step 5. Write
`contracts/alternatives.yaml` (at least 3 system alternatives, POLICY section 2) and run `--alternatives`. If
the winner uses a principle an owner did not recommend, send SWITCH and wait for its report. Then `--release`
must be green, or each remaining error is recorded as an open issue.
**F. T+51-60 Package; at the concept tier then STOP.**
Complete `deliverables/02-concept/concept.md` (5.1); run
`--compliance deliverables/01-requirements/compliance.csv` and review it (statuses are predictions; honest): a
budget-linked row is judged against the budget's limit, so where that limit is not the requirement's threshold
write a SYS entry; no row stays `not_addressed` (owner FIX or SYS entry); write `deliverables/REPORT.md` (handbook
section 15 plus a contract status section: version, check result, budget table, amendments, open
non-conformances); optionally `deliverables/09-bom-cost/cost.json` rolled up from the owners' `bom.csv` and SYS;
`deliverables/manifest.json` (section 7); run `python3 method/gates/dd_check.py --tier concept --run-scripts` and
fix every ERROR. Concept tier: finish (section 9) and never start CAD, drawings, electronics, firmware or renders
(outside this tier's scope, RUN CONTRACT section 1). Full tier: continue with section 6, F1.
Clock rules: past T+75 accept no new amendments (open ones are rejected with reason "time" and listed as open
issues) and skip SWITCH; at T+90 go straight to F with what exists; finish by T+110.
### 5.1 What 01 and 02 contain at the concept tier
- `01-requirements/`: `requirements.md` (given requirements with interpretation, consequence and allocation;
derived requirements; conflicts and decisions; assumptions register) and `compliance.csv` (generated, then
reviewed).
- `02-concept/concept.md` in the order of handbook section 5, with this method visible: section 3 alternatives =
the candidate partitions (architectures) and the system alternatives (principle combinations) with the
owners' numbers; section 4 evaluation = knock-outs, budget closure, weighted score and sensitivity (from
`contracts/alternatives.md`); section 5 chosen concept = the architecture (`contracts/architecture.mmd` as a
Mermaid block plus `architecture.png`), subsystems with make-or-buy and the owners' critical parts, the
interface summary (link `contracts/REGISTRY.md`); section 6 key estimates = the budget table (allocation,
actual, margin per subsystem and for the system) with links to the calculation scripts; section 10 risks
include the contract risks (thinnest margins, coupled blocks, open amendments).
- Also in `02-concept/`: `partitions.md`, `physics-map.md`, `benchmark.md`, `calc/` (system-level ddpy scripts
with results files), `contracts/` (registry, REGISTRY.md, dsm.csv, architecture files, CHANGELOG.md,
history/, candidates/, amendments/, alternatives.*) and `subsystems/<S>/` (each owner's concept.md, calc/,
bom.csv, conformance.yaml, STATE.md) plus `subsystems/SYS/conformance.yaml`.
## 6. Full tier: the complete package (plan about 8-9 hours)
- **F0 T+0:00-1:05** section 5 A-F without stopping; write manifest.json with `"tier": "full"` from the start and
leave the dd_check run of step F to the release. 01, 02, REPORT.md v1 and the manifest exist after hour one.
- **F1 T+1:05-1:25 Detail contract.** The next free `AMD-SYS-<nn>` with `proposed_registry: <that id>` (the full
proposed YAML files in `contracts/amendments/<that id>/`): exact mounting features and positions in the product
frame, connector parts, pin maps with net names and `refs`, protocols and message sets, thermal limits at
interfaces, `bom_quantity` on the cost budget, envelopes and allocations refined from the concept numbers,
reserves per POLICY section 3. Route it to all owners (resume the concept owners) and merge.
- **F2 T+1:25-4:00 Round DETAIL-1** (fresh owners, one message): CAD parts and the sub-assembly in the product
frame, complete real-part BOM, schematic, mass from CAD, conformance. Meanwhile you write
`03-cad/assembly.py` (imports every `03-cad/<S>/subassembly.step`, names the parts by subsystem, writes
`assembly.step`), the system hazard analysis and regulatory map for 08, the 09 roll-up script and the 10
skeleton.
- **F3 T+4:00-4:30 Integration 1.** `integration_check.py` and the contract check; FIX messages and amendments;
assembly and interference.
- **F4 T+4:30-6:45 Round DETAIL-2** (fresh owners): board layout, DRC and fabrication outputs, firmware (the
`owns_firmware` owner), 05 calculations, 04 drawings, `hazards.md` and `tests.md`, conformance.
- **F5 T+6:45-8:15 Integration 2 and the system packages.** `assembly.step`, then `step2glb` to
`11-renders/assembly.glb` and Blender renders from it (handbook section 14); the assembly drawing (04); 08
complete (owners' hazards.md merged into the system analysis, directives, standards, route, lab tests); 09
(`bom.csv` = the owners' bom.csv merged with item numbers plus SYS lines, `cost.json`, `cost.md`); 10 (build
plan, bring-up, one test per requirement and one per interface parameter of REGISTRY.md, the owners'
tests.md); REPORT.md complete; manifest.
- **F6 T+8:15-8:45 Release:** contract `--release`, `integration_check.py`, `dd_check.py --tier full --run-scripts`;
fix; finish.
Locations: owners write `03-cad/<S>/`, `04-drawings/<S>/`, `05-calculations/<S>-<topic>.py` (flat: the RUN
CONTRACT runs these scripts from that folder), `06-electronics/<S>/` (one KiCad project per subsystem with
electronics); `07-firmware/` is one PlatformIO project owned by the `owns_firmware` subsystem. You write
`03-cad/assembly.*`, `04-drawings/assembly.*`, `05-calculations/SYS-*.py`, 08, 09, 10, 11, REPORT.md and the
manifest. Behind schedule: cut depth (fewer secondary drawings, simpler renders), never honesty; release by
T+12 h at the latest.
## 7. Gates
- **G1 contract freeze:** `--freeze` green for v1; every later version only through `--merge`, which re-checks.
- **G2 owner self-check:** `--subsystem <S>` without ERROR (or only one that waits for a routed amendment)
before an owner reports done.
- **G3 concept release:** `contract_check.py --release` green (it also requires met / partly / not_met for
every requirement id in some conformance file), compliance.csv generated and reviewed,
`dd_check.py --tier concept --run-scripts` PASS.
- **G4 integration (full tier):** `integration_check.py` green after DETAIL-1 and DETAIL-2, or every remaining
error listed in REPORT.md with its reason.
- **G5 full release:** G3, G4 and `dd_check.py --tier full --run-scripts` PASS.
A failing gate is fixed at its cause: never edit a check script, never loosen a contract value just to pass,
never relabel a FAIL. What cannot be fixed in time is reported (PARTIAL / FAIL / BLOCKED / NOT RUN).
`manifest.json` follows the RUN CONTRACT format and lists every file under `deliverables/`: generate the list
with a short script of your own and describe each file in a few words; `key_figures` come from the budget
roll-up and cost.json (null when unknown).
## 8. Failure handling
- `subsystem-owner` type unknown: use `subagent_type: "general-purpose"` with the prompt prefixed "First read
.claude/agents/subsystem-owner.md and follow it as your role definition."
- An Agent call is refused or errors: retry it once a minute later; if it fails again, run that owner's pass
yourself at reduced depth (same files, same checks) and record it in ARCH-STATE.md and REPORT.md.
- An owner ends without `conformance.yaml`, crashes or reports blocked: relaunch it once (fresh, same prompt
plus "previous attempt: <what is missing>; resume from your folder"). A second failure: take the subsystem
over yourself at reduced depth and record that in ARCH-STATE.md and REPORT.md.
- An owner still running 8 minutes past its end time: stop it with TaskStop (loaded by ToolSearch
"select:TaskStop") and use what is in its folder. If it is the last one running, nothing else will wake you:
do step E work for the others, then make one bounded foreground wait (as in 4.2) until that moment and stop
it. Never end the run with an agent still running.
- SendMessage unavailable: run votes, fixes and switches as short fresh owner tasks (VOTE prompt in
owner-tasks.md); owners then put proposals only in the amendment files and their final report.
- `contract_check.py` stops with a traceback: it is almost always a YAML or field error in your input; fix
that. If the tool itself is broken, do its checks by hand (allocations within limit minus reserve, envelopes
disjoint, every id allocated, every obligation reported and within bounds), log "manual check" in
CHANGELOG.md and REPORT.md, and continue.
- A toolchain route fails three times: the labelled fallback of handbook section 1 and a `blocked[]` entry if
it mattered.
- Running out of time: the clock rules of sections 5 and 6. The package stays valid at every moment: run
dd_check after every larger change.
## 9. Finishing
1. `date`. Every agent you started has reported or been stopped (owners table); no loops, crons or background
jobs remain.
2. Final checks: contract `--release`; at the full tier `integration_check.py`; `python3
method/gates/dd_check.py --tier <tier> --run-scripts` PASS.
3. Re-read REPORT.md and concept.md against the numbers' homes (handbook section 15).
4. Update `state/ARCH-STATE.md` (final state).
5. End with the typed JSON final report your Oivo contract asks for and `GUI_STATE` with `state: done`
(RUN CONTRACT section 6).
agents/subsystem-owner.md (11 wierszy)
--- name: subsystem-owner description: Owner of one subsystem in a CONTRACT-method hardware design run. Designs the whole subsystem (mechanics, electronics, firmware, parts, cost) against the frozen contract registry, reports conformance, proposes amendments and votes on other owners' amendments. Use for every subsystem-owner task the architect launches. model: opus omitClaudeMd: true --- You are a subsystem owner in a hardware design run with no human in the loop. Before anything else read `method/HYGIENE.md` and `method/roles/subsystem-owner.md` (paths relative to the run directory named in your task) and follow them exactly. Your task names your subsystem, the pass, the contract version and the deadline. Decide everything yourself: nobody answers questions. End with the OWNER REPORT block defined in the role file.
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 (94 wierszy)
# Decision policy - what replaces the human in this run
Read by the architect and by every subsystem owner. Record every use of a default in concept.md section 11
("decisions taken without a human") with the alternatives you rejected.
## 1. Priorities when goals conflict (the higher one wins)
1. Safety of users and bystanders, including foreseeable misuse; protections that work without firmware.
2. Legal sale in the target markets (EU CE route; US FCC rules and the safety listing retailers expect) by a
standard conformity route.
3. Requirement thresholds, by priority (5 first). A predicted miss is `not_met` with its number, never hidden.
4. Budget closure: every budget closes with its reserve.
5. Unit cost at the quantity the requirements emphasise, then the retail price.
6. Requirement targets, by priority.
7. Simplicity: fewer subsystems, interfaces and custom parts; standard parts stocked by two or more distributors.
## 2. Decision rules
**Partition (architect, step B).** Valid: 2-5 subsystems; each is a physical module with its own envelope that
owns everything inside its boundary across disciplines (its parts, its board, its code); never a split by
discipline ("all electronics", "all mechanics"); every subsystem carries at least one requirement id.
If the product does not split cleanly, use two: the core function, and the enclosure with the user interface.
Knock-out: a candidate whose estimates do not close a budget whose limit comes from a requirement. Pick: the
largest minimum budget margin, unless another survivor has a coupling index at least 25 % lower within 5 margin
points (`--candidates` prints this suggestion); tie: fewer subsystems. You may override the suggestion only with
a written reason in partitions.md. Budget margins compare in % of the limit; temperature caps (offset scale)
and dB budgets count only as knock-outs, their margins are reported in K or dB.
**Principles (owner, per subsystem).** Exactly two that differ in working principle, not in material or colour.
Recommend by: the allocated thresholds are met; then it fits every budget allocation; then it is better on the
highest-priority allocated requirements; then lower risk.
**System alternatives (architect, step E).** At least three combinations of the owners' principles that differ
in the working principle of at least one critical subsystem; the losing partitions appear as architecture
alternatives at sketch level. Knock-out by predicted thresholds and by budget closure (`--alternatives`).
Criteria are requirement groups (performance, safety and compliance, cost, size and mass, user experience,
reliability and service ...); weight = the highest priority among the group's ids; each 0-10 score is
justified with a number in concept.md. Winner: the highest weighted score; among alternatives within 0.3 points
of it, the larger minimum budget margin (`--alternatives` applies this rule). If the +-20 % weight sensitivity
flips the winner, say so and prefer the larger minimum margin unless the numbers clearly favour the other.
**Amendment votes (owners).** ACCEPT when your subsystem still meets every obligation and allocation after the
change (name the number that still holds). REJECT with the number that breaks, plus a counter-proposal when you
have one. Never reject for convenience; never accept what you cannot meet.
**System budget violation (architect - the only exception to routing).** Resolve in this order: (1) the
unallocated reserve; (2) move allocation from a subsystem with spare margin (that owner votes, normal route);
(3) switch the offending subsystem to its other principle if that closes the budget (SWITCH message); (4)
change a derived limit. Only steps 1 and 4 are merged without votes. A requirement threshold is never relaxed:
if it cannot be met, the requirement is `not_met` and the report says so.
## 3. Budgets and reserves
- Always: `mass` (kg); unit cost at the quantity the requirements emphasise (q1000 if none; the currency of the
requirements, else EUR); `input_power` (W) for a powered product. Add one budget per requirement whose metric
adds up over subsystems (standby power, heat into the enclosure, battery energy per use, sound with
`combine: db`) and one per cap every subsystem must respect (touch temperature with `combine: max`).
- `limit` = the requirement threshold or a derived limit with its origin; `goal` = the requirement target.
- Reserves at the concept freeze: mass 10 %, cost 15 %, power 10 %, heat 15 %, sound 2 dB (`reserve_abs`),
temperature caps 5 K (`reserve_abs`). In the full-tier detail contract halve them where actuals now come from
CAD or real BOM prices.
- Allocation of a subsystem = its estimate plus a share of the remaining margin in proportion to its
uncertainty; never allocate the reserve. SYS holds assembly and test labour, packaging, certification and
tooling amortisation in the cost budget.
## 4. Taste defaults (when nothing above decides)
- Markets: EU first, then US. SI units; English for UI text and manuals.
- Power: an external certified power supply when the power level allows; safety extra-low voltage inside
wherever possible; a certified power module rather than a self-designed mains supply.
- Radio: pre-certified modules only.
- Materials: no exotic materials; flame-rated plastics (UL 94 V-0) near heat and live parts; colour carried by
the material, matte dark grey or black.
- Form: simple prismatic forms that serve the function; no decorative features that add tooling cost.
- Manufacturing: prototype by 3D printing, CNC and laser-cut sheet; 1000 units by injection moulding, sheet metal
or extrusion where the tooling amortises (state the break-even quantity).
- Parts: active (not end-of-life), stocked by two or more distributors, a second source for critical parts,
metric fasteners from a small set.
- Use and service: works out of the box without an app; wear parts replaceable with common tools; safe state at
power-up and on every fault.
- Price: retail = landed unit cost x 2.5-3 unless the benchmark or a requirement says otherwise; check
platform and payment fees, VAT, shipping to backers and returns.
## 5. Time plan and stop rules
- Concept tier: the checkpoints of MAIN section 5. Stop after `01-requirements/`, `02-concept/`, `REPORT.md` and
`manifest.json` (optionally `09-bom-cost/cost.json`). Never start detailed design at this tier.
- Full tier: the checkpoints of MAIN section 6; release around T+9 h, at T+12 h at the latest.
- Owners end at the deadline in their task. TIME means: complete conformance.yaml with what you have (uncertain
values labelled ASSUMPTION), self-check, end within 3 minutes.
- Behind schedule: cut depth, not honesty, and never the method: the contract, two principles per subsystem,
routed amendments and the release check stay.
- Never wait for a person; never end with an agent still running.
files/method/gates/contract_check.py (1479 wierszy)
#!/usr/bin/env python3
"""contract_check.py - the executable contract registry of a CONTRACT-method run.
Run: ddpy method/gates/contract_check.py [MODE] [--json] (it always works in the run directory it lives in,
<run>/method/gates/, whatever the current directory; paths given as arguments are taken as typed)
Registry: deliverables/02-concept/contracts/{interfaces,budgets,allocation}.yaml + VERSION (architect only).
Owners: deliverables/02-concept/subsystems/<S>/conformance.yaml (owner of S only). Formats: method/templates/.
Modes (default = check):
(none) [--subsystem S] [--release] registry, conformance, budget roll-up, amendments
--ids allocation skeleton of the requirement ids in INPUT/requirements.md
--candidates compare contracts/candidates/*.yaml (coupling, budget closure, suggested pick)
--freeze NOTE contract-level check; if green: VERSION+1, history/vN/, dsm.csv, REGISTRY.md,
architecture.mmd/.png, CHANGELOG.md (refused once owners conform: amend instead)
--brief S contract view of subsystem S + conformance skeleton (owners start here)
--route AMD validate and dry-run an amendment (changes: op set | add | remove, see
apply_change); list the owners who must vote
--vote AMD S ACCEPT|REJECT WHY --merge AMD --reject AMD WHY
--alternatives budget closure + weighted score + sensitivity of contracts/alternatives.yaml
--compliance OUT.csv compliance.csv: a SYS entry wins, then the budget roll-up for ids allocated with
'budget:', then the worst owner status
Exit code 0 = no ERROR.
"""
import argparse
import copy
import csv
import datetime as dt
import fnmatch
import glob
import json
import math
import os
import re
import shutil
import sys
def _ensure_libs(): # the system python3 may lack pint: re-run once under the toolchain Python
try:
import pint # noqa: F401
import yaml # noqa: F401
except ImportError:
for cand in (shutil.which("ddpy"), "/opt/dd/venv/bin/python"):
if cand and os.path.exists(cand) and not os.environ.get("CC_REEXEC"):
os.environ["CC_REEXEC"] = "1"
os.execv(cand, [cand] + sys.argv)
_ensure_libs()
try:
import yaml
except ImportError:
sys.exit("contract_check: PyYAML missing - run: ddpy method/gates/contract_check.py")
try:
import pint
except ImportError:
pint = None
DELIV = "deliverables"
REG_DIR = os.path.join(DELIV, "02-concept", "contracts")
SUB_DIR = os.path.join(DELIV, "02-concept", "subsystems")
SECTIONS = {"interfaces": ("frame", "product_envelope_mm", "subsystems", "envelopes", "interfaces"),
"budgets": ("budgets",), "allocation": ("allocation",)}
KINDS = {"mechanical", "electrical", "thermal", "fluid", "data", "optical", "acoustic", "user", "software",
"magnetic", "chemical", "other"}
RULES, COMBINE = ("within", "covers", "equals"), ("sum", "max", "db")
STATUSES = ("met", "partly", "not_met", "not_addressed")
AMD_STATUS = ("proposed", "routed", "accepted", "rejected", "merged", "withdrawn")
ID_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,31}$")
HEADING_ID = re.compile(r"^(#{1,6})\s+([A-Z][A-Z0-9]{1,7}-[A-Z0-9]{1,7}-\d{1,4})\b[\s—–:-]*(.*)$")
LOOSE_ID = re.compile(r"\b([A-Z][A-Z0-9]{1,7}-[A-Z]{2,7}-\d{2,4})\b")
PRIO_RE = re.compile(r"(?i)^\W*(?:priority|weight)\W{0,3}[:=]\W{0,3}(\d+(?:\.\d+)?)")
CURRENCIES = ("EUR", "USD", "PLN", "GBP", "CNY", "CHF", "JPY", "CAD", "AUD", "SEK", "NOK", "DKK", "CZK", "HKD")
COUNT_UNITS = {"pcs", "pc", "piece", "pieces", "unit", "units", "item", "items", "count", "ea", "each"}
REQS = [] # [(id, title, priority)] of INPUT/requirements.md
WINDOW = 0.3 # POLICY section 2: alternatives within 0.3 weighted points are decided by the minimum budget margin
LIST_KEYS = {"subsystems", "envelopes", "interfaces", "params", "budgets", "consumers", "boxes", "principles"}
def anchor_run_dir():
"""Work in the run directory this file lives in (<run>/method/gates/), whatever the caller's cwd."""
run = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if os.path.isdir(os.path.join(run, "INPUT")) and os.path.realpath(os.getcwd()) != os.path.realpath(run):
os.chdir(run)
return run
class Report:
"""ERROR/WARN/NOTE lines. With focus=S only S's problems count; contract-level ones become notes."""
def __init__(self, focus=None):
self.focus, self.errors, self.warnings, self.notes = focus, [], [], []
def _add(self, bucket, msg, sub):
if not self.focus or sub == self.focus:
bucket.append(msg)
elif sub is None:
self.notes.append("(architect) " + msg)
def error(self, msg, sub=None):
self._add(self.errors, msg, sub)
def warn(self, msg, sub=None):
self._add(self.warnings, msg, sub)
def note(self, msg):
self.notes.append(msg)
# ------------------------------------------------------------------------------------------------ helpers
def now():
return dt.datetime.now().astimezone().isoformat(timespec="minutes")
def num(x):
if isinstance(x, bool) or x is None:
return None
if isinstance(x, (int, float)):
return float(x)
try:
return float(str(x).strip())
except ValueError:
return None
def fmt(x):
return "?" if x is None else f"{x:.4g}"
def read_yaml(path, rep=None, sub=None):
"""Parsed YAML ({} if empty); None if missing or broken (broken is reported)."""
try:
with open(path, encoding="utf-8") as fh:
data = yaml.safe_load(fh)
return {} if data is None else data
except FileNotFoundError:
return None
except yaml.YAMLError as exc:
if rep is not None:
rep.error(f"{path}: YAML error: {' '.join(str(exc).split())[:200]}", sub)
return None
def dump_yaml(path, data):
with open(path, "w", encoding="utf-8") as fh:
yaml.safe_dump(data, fh, sort_keys=False, allow_unicode=True, width=110, default_flow_style=None)
_UREG = []
def unit_of(u):
"""('pint', unit) or ('lit', text); dB-type and count units are compared as text, never converted."""
if pint is not None and not _UREG:
_UREG.append(pint.UnitRegistry(autoconvert_offset_to_baseunit=True))
_UREG[0].define("Nm = newton * meter")
for cur in CURRENCIES:
_UREG[0].define(f"{cur} = [currency_{cur}]")
s = "" if u is None else str(u).strip()
s = "dimensionless" if s.lower() in ("", "1", "-", "none") else s
low = re.sub(r"\s+", "", s.lower())
if not _UREG or s.startswith("dB") or low in COUNT_UNITS:
return ("lit", low)
try:
return ("pint", _UREG[0].Quantity(1, s.upper() if s.upper() in CURRENCIES else s).units)
except Exception: # noqa: BLE001 - unparsable means "compare as text"
return ("lit", low)
def is_offset(u):
k = unit_of(u)
return k[0] == "pint" and str(k[1]) in ("degree_Celsius", "degree_Fahrenheit")
def convert(x, src, dst):
a, b = unit_of(src), unit_of(dst)
if a[0] == "lit" or b[0] == "lit":
if a == b:
return x
raise ValueError(f"unit '{src}' is not comparable with '{dst}'")
try:
return float(_UREG[0].Quantity(x, a[1]).to(b[1]).magnitude)
except Exception as exc: # noqa: BLE001
raise ValueError(f"unit '{src}' cannot be converted to '{dst}' ({type(exc).__name__})") from None
def as_range(v):
if isinstance(v, (list, tuple)) and len(v) == 2 and None not in (num(v[0]), num(v[1])):
return tuple(sorted((num(v[0]), num(v[1]))))
return None if num(v) is None else (num(v), num(v))
def bounds(p):
lo, hi, nom = num(p.get("min")), num(p.get("max")), num(p.get("nominal"))
if nom is not None:
tol, tolp = num(p.get("tol")), num(p.get("tol_pct"))
t = tol if tol is not None else abs(nom) * tolp / 100 if tolp is not None else 0.0
lo, hi = (nom - t if lo is None else lo), (nom + t if hi is None else hi)
return lo, hi
def bounds_txt(p):
if p.get("rule") == "equals":
return str(p.get("value", p.get("pins")))
lo, hi = bounds(p)
return f"<= {fmt(hi)}" if lo is None else f">= {fmt(lo)}" if hi is None else f"[{fmt(lo)}, {fmt(hi)}]"
def norm(v):
if isinstance(v, dict):
return {str(k).strip().lower(): norm(x) for k, x in v.items()}
if isinstance(v, (list, tuple)):
return [norm(x) for x in v]
return num(v) if num(v) is not None else re.sub(r"\s+", " ", str(v)).strip().lower()
def is_ext(x):
return isinstance(x, str) and x.startswith("EXT:")
def cons_of(itf):
c = itf.get("consumers") or []
return [c] if isinstance(c, str) else list(c)
def parties(itf):
return [itf.get("provider")] + cons_of(itf)
def param_party(itf, p):
"""Who must satisfy a param: within -> provider, covers -> consumers, equals -> everyone (or 'party')."""
rule, party = p.get("rule", "within"), p.get("party")
if party is None:
party = [itf.get("provider")] if rule == "within" else cons_of(itf) if rule == "covers" else parties(itf)
return [party] if isinstance(party, str) else list(party)
def sub_ids(reg):
return [str(s["id"]) for s in reg.get("subsystems") or [] if isinstance(s, dict) and s.get("id")]
def reserve_of(b):
lim = abs(num(b.get("limit")) or 0.0)
if num(b.get("reserve_abs")) is not None:
return num(b["reserve_abs"])
if num(b.get("reserve_pct")) is not None:
return lim * num(b["reserve_pct"]) / 100
return lim * 0.10 if b.get("combine", "sum") == "sum" else 0.0
def combine(kind, values):
vals = [v for v in values if v is not None]
if not vals:
return None
if kind == "max":
return max(vals)
return 10 * math.log10(sum(10 ** (v / 10) for v in vals)) if kind == "db" else sum(vals)
def pct_ok(b):
"""Percent margins only on a ratio scale: not for dB sums, not for temperatures on an offset scale (degC)."""
return b.get("combine", "sum") != "db" and not is_offset(b.get("unit"))
def closure(b, values):
"""(total, margin, margin_pct) of budget b for {subsystem: value in the budget unit}; pct None if not pct_ok."""
lim, kind = num(b.get("limit")), b.get("combine", "sum")
tot = combine(kind, values.values())
if lim is None or tot is None:
return tot, None, None
return tot, lim - tot, None if not pct_ok(b) or lim == 0 else 100 * (lim - tot) / abs(lim)
def diff_unit(u):
"""Unit of a difference of two values in u: degC -> K, degF -> delta_degF, else u."""
k = unit_of(u)
if k[0] == "pint" and str(k[1]) == "degree_Celsius":
return "K"
return "delta_degF" if k[0] == "pint" and str(k[1]) == "degree_Fahrenheit" else u
def margin_txt(margin, pct, unit):
return "?" if margin is None else f"{pct:+.1f} %" if pct is not None else f"{margin:+.3g} {diff_unit(unit)}"
def min_txt(m):
return f"{m:.1f} %" if m is not None and m > -1e8 else "n/a"
def over(margin, b):
"""Negative margin beyond rounding noise."""
return margin is not None and margin < -1e-9 * max(1.0, abs(num(b.get("limit")) or 0.0))
# ------------------------------------------------------------------------------------------- requirements
def requirement_info(path=os.path.join("INPUT", "requirements.md")):
"""[(id, title, priority)]: heading ids first (as dd_check.py does), loose tokens as a fallback."""
if not os.path.isfile(path):
return []
lines, out, seen = open(path, encoding="utf-8", errors="replace").read().splitlines(), [], set()
for i, line in enumerate(lines):
m = HEADING_ID.match(line.strip())
if not m or m.group(2) in seen:
continue
seen.add(m.group(2))
prio = None
for nxt in (x.strip() for x in lines[i + 1:]):
if nxt.startswith("#") and len(nxt) - len(nxt.lstrip("#")) <= len(m.group(1)):
break
pm = PRIO_RE.search(nxt)
if pm:
prio = float(pm.group(1))
break
out.append((m.group(2), m.group(3).strip(), prio))
if not out:
for m in LOOSE_ID.finditer("\n".join(lines)):
if m.group(1) not in seen:
seen.add(m.group(1))
out.append((m.group(1), "", None))
return out
def alloc_targets(v):
v = v.get("to", []) if isinstance(v, dict) else v
return [str(x) for x in ([v] if isinstance(v, str) else v or [])]
def allocation_for(alloc, rid):
"""An exact key wins; otherwise the union of every matching pattern."""
if rid in alloc:
return alloc_targets(alloc[rid])
out = []
for k, v in alloc.items():
if any(ch in str(k) for ch in "*?[") and fnmatch.fnmatchcase(rid, str(k)):
out += [x for x in alloc_targets(v) if x not in out]
return out
def alloc_budget(alloc, rid):
"""The budget an allocation entry links to rid (long form 'budget:'; exact key first, then patterns) or None."""
keys = [rid] if rid in alloc else [k for k in alloc if any(ch in str(k) for ch in "*?[")
and fnmatch.fnmatchcase(rid, str(k))]
return next((str(alloc[k]["budget"]) for k in keys if isinstance(alloc[k], dict) and alloc[k].get("budget")), None)
# ----------------------------------------------------------------------------------------------- registry
def sections_of(data):
return {k: data[k] for keys in SECTIONS.values() for k in keys if isinstance(data, dict) and k in data}
def load_registry(regdir, rep):
reg = {}
for name, keys in SECTIONS.items():
path = os.path.join(regdir, name + ".yaml")
data = read_yaml(path, rep)
if data is None and not os.path.exists(path):
rep.error(f"{path} missing")
elif data is not None and not isinstance(data, dict):
rep.error(f"{path} must be a mapping")
elif data is not None:
reg.update({k: v for k, v in data.items() if k in keys})
for k in (k for k in data if k not in keys):
rep.warn(f"{path}: unknown key '{k}' ignored (allowed: {', '.join(keys)})")
return reg
def load_snapshot(regdir, v):
snap = os.path.join(regdir, "history", f"v{v}")
if not os.path.isdir(snap):
return None
reg = {}
for name in SECTIONS:
reg.update(sections_of(read_yaml(os.path.join(snap, name + ".yaml")) or {}))
return reg
def write_registry(regdir, reg):
for name, keys in SECTIONS.items():
dump_yaml(os.path.join(regdir, name + ".yaml"), {k: reg[k] for k in keys if k in reg})
def read_version(regdir):
try:
return int(open(os.path.join(regdir, "VERSION")).read().strip() or 0)
except (OSError, ValueError):
return 0
def check_subsystems(reg, rep):
ids = []
for s in reg.get("subsystems") or []:
sid = str(s.get("id")) if isinstance(s, dict) and s.get("id") else None
if not sid:
rep.error(f"subsystems: entry without id: {str(s)[:80]}")
continue
if not ID_RE.match(sid) or sid == "SYS" or sid.startswith("EXT"):
rep.error(f"subsystems: id '{sid}' invalid (letters, digits, '-', '_'; SYS and EXT are reserved)")
if sid in ids:
rep.error(f"subsystems: duplicate id {sid}")
ids.append(sid)
for k in ("name", "scope"):
if not s.get(k):
rep.warn(f"subsystems: {sid} has no {k}")
if len(ids) < 2:
rep.error(f"subsystems: {len(ids)} found; the method needs at least 2")
if len(ids) > 5:
rep.warn(f"subsystems: {len(ids)}; the policy allows at most 5 owners")
fw = [str(s.get("id")) for s in reg.get("subsystems") or [] if isinstance(s, dict) and s.get("owns_firmware")]
if len(fw) > 1:
rep.error(f"subsystems: owns_firmware on {', '.join(fw)}; exactly one subsystem owns 07-firmware/")
elif ids and not fw:
rep.warn("subsystems: no subsystem has owns_firmware: true, so nobody owns 07-firmware/")
return ids
def check_param(itf, p, names, valid, rep):
if not isinstance(p, dict) or not p.get("name"):
return rep.error(f"{itf.get('id')}: param without name: {str(p)[:80]}")
pn, rule = f"{itf.get('id')}.{p['name']}", p.get("rule", "within")
if p["name"] in names:
rep.error(f"{pn}: duplicate param name")
names.add(p["name"])
if rule not in RULES:
return rep.error(f"{pn}: rule '{rule}' not in {RULES}")
if rule == "equals" and "value" not in p and "pins" not in p:
rep.error(f"{pn}: rule equals needs value or pins")
if rule != "equals":
lo, hi = bounds(p)
if lo is None and hi is None:
rep.error(f"{pn}: needs min and/or max (or nominal with tol / tol_pct)")
elif None not in (lo, hi) and lo > hi:
rep.error(f"{pn}: min {fmt(lo)} > max {fmt(hi)}")
if "unit" not in p:
rep.error(f"{pn}: unit missing (use '1' for a plain number)")
elif unit_of(p["unit"])[0] == "lit":
rep.warn(f"{pn}: unit '{p['unit']}' is not a pint unit; compared as text")
party = param_party(itf, p)
for x in party:
if not is_ext(x) and x not in valid:
rep.error(f"{pn}: party '{x}' is not a subsystem id")
elif x not in parties(itf):
rep.warn(f"{pn}: party '{x}' is not a party of {itf.get('id')}")
if not party or all(is_ext(x) for x in party):
rep.warn(f"{pn}: no subsystem has to conform to it")
if not p.get("origin"):
rep.warn(f"{pn}: no origin (SOURCE / CALCULATION / ASSUMPTION + reason)")
def check_interfaces(reg, rep, subs, sketch=False):
items, seen = reg.get("interfaces"), set()
if not isinstance(items, list) or not items:
return rep.error("interfaces: missing or empty list")
for itf in items:
if not isinstance(itf, dict) or not itf.get("id"):
rep.error(f"interfaces: entry without id: {str(itf)[:80]}")
continue
iid = str(itf["id"])
if iid in seen:
rep.error(f"interfaces: duplicate id {iid}")
seen.add(iid)
if itf.get("kind") not in KINDS:
rep.error(f"{iid}: kind '{itf.get('kind')}' not in {sorted(KINDS)}")
if not itf.get("provider") or not cons_of(itf):
rep.error(f"{iid}: needs a provider and at least one consumer")
allp = [x for x in parties(itf) if x]
for x in allp:
if not is_ext(x) and x not in subs:
rep.error(f"{iid}: party '{x}' is not a subsystem id or EXT:<name>")
if allp and all(is_ext(x) for x in allp):
rep.error(f"{iid}: no internal party")
if not itf.get("params") and not sketch:
rep.warn(f"{iid}: no params, nothing checkable")
names = set()
for p in itf.get("params") or []:
check_param(itf, p, names, set(subs), rep)
def boxes_of(e):
b = e.get("boxes") if isinstance(e, dict) else None
return ([b] if isinstance(b, list) and b and not isinstance(b[0], (list, tuple)) else b) or []
def box_ok(b):
return (isinstance(b, (list, tuple)) and len(b) == 6 and None not in [num(x) for x in b]
and all(num(b[i]) < num(b[i + 3]) for i in range(3)))
def box_inside(b, outer, tol=0.01):
return all(b[i] >= outer[i] - tol and b[i + 3] <= outer[i + 3] + tol for i in range(3))
def box_overlap(a, b, tol=0.01):
d = [min(a[i + 3], b[i + 3]) - max(a[i], b[i]) for i in range(3)]
return d[0] * d[1] * d[2] if all(x > tol for x in d) else 0.0
def check_envelopes(reg, rep, subs):
env, pe = reg.get("envelopes") or [], reg.get("product_envelope_mm")
if pe is not None and not box_ok(pe):
rep.error("product_envelope_mm must be [x0, y0, z0, x1, y1, z1] with x0 < x1, y0 < y1, z0 < z1")
pe = None
claims, named = [], set()
for e in env:
sid = e.get("subsystem") if isinstance(e, dict) else None
if sid not in subs:
rep.error(f"envelopes: '{sid}' is not a subsystem id")
continue
named.add(sid)
if not boxes_of(e):
rep.error(f"envelopes: {sid} has no boxes")
for b in boxes_of(e):
if not box_ok(b):
rep.error(f"envelopes: {sid} box {b} must be [x0, y0, z0, x1, y1, z1] with x0 < x1 ...")
continue
b = [num(x) for x in b]
if pe and not box_inside(b, [num(x) for x in pe]):
rep.error(f"envelopes: {sid} box {b} is outside product_envelope_mm {pe}")
if not e.get("shell"):
claims.append((sid, b))
for i, (sa, ba) in enumerate(claims):
for sb, bb in claims[i + 1:]:
if sa != sb and box_overlap(ba, bb) > 0:
rep.error(f"envelopes: {sa} and {sb} overlap by {box_overlap(ba, bb):.0f} mm3")
missing = [s for s in subs if s not in named]
if missing:
rep.warn(f"envelopes: no space claim for {', '.join(missing)}")
def check_budgets(reg, rep, subs, sketch=False):
items, names = reg.get("budgets"), set()
if not isinstance(items, list) or not items:
return rep.error("budgets: missing or empty list")
for b in items:
if not isinstance(b, dict) or not b.get("name"):
rep.error(f"budgets: entry without name: {str(b)[:80]}")
continue
n, kind, unit, lim = b["name"], b.get("combine", "sum"), b.get("unit"), num(b.get("limit"))
if n in names:
rep.error(f"budget {n}: duplicate name")
names.add(n)
if kind not in COMBINE:
rep.error(f"budget {n}: combine '{kind}' not in {COMBINE}")
if lim is None or unit is None:
rep.error(f"budget {n}: needs a numeric limit and a unit")
elif unit_of(unit)[0] == "lit" and kind != "db":
rep.warn(f"budget {n}: unit '{unit}' is not a pint unit; compared as text")
if kind == "sum" and is_offset(unit):
rep.error(f"budget {n}: temperatures cannot be summed; use combine: max")
if b.get("reserve_pct") is None and b.get("reserve_abs") is None:
rep.warn(f"budget {n}: no reserve_pct / reserve_abs (default {'10 %' if kind == 'sum' else '0'})")
if not b.get("origin"):
rep.warn(f"budget {n}: no origin for the limit")
allocs = b.get("allocations") or {}
if not isinstance(allocs, dict) or (not allocs and not sketch):
rep.error(f"budget {n}: allocations must map subsystem -> number")
continue
for s, v in allocs.items():
if s not in subs and s != "SYS" or num(v) is None:
rep.error(f"budget {n}: allocation {s}: {v!r} (subsystem id or SYS -> number)")
tot = combine(kind, [num(v) for v in allocs.values()])
if tot is not None and lim is not None and tot > lim - reserve_of(b) + 1e-9 * max(1.0, abs(lim)):
rep.error(f"budget {n}: allocations {fmt(tot)} {unit} exceed limit {fmt(lim)} - reserve "
f"{fmt(reserve_of(b))} = {fmt(lim - reserve_of(b))} {unit}")
for s, v in (b.get("estimates") or {}).items():
if s in allocs and None not in (num(v), num(allocs[s])) and num(v) > num(allocs[s]):
rep.warn(f"budget {n}: estimate of {s} ({v}) exceeds its allocation ({allocs[s]})")
def check_allocation(reg, rep, subs, sketch=False):
alloc = reg.get("allocation")
if not isinstance(alloc, dict) or not alloc:
return (rep.warn if sketch else rep.error)("allocation: missing or empty mapping")
budgets = {b.get("name") for b in reg.get("budgets") or [] if isinstance(b, dict)}
itfs = {i.get("id") for i in reg.get("interfaces") or [] if isinstance(i, dict)}
for k, v in alloc.items():
if not alloc_targets(v):
rep.error(f"allocation: {k} is allocated to nobody")
for x in alloc_targets(v):
if x not in subs and x != "SYS":
rep.error(f"allocation: {k} -> unknown '{x}' (subsystem id or SYS)")
if isinstance(v, dict) and (v.get("budget") and v["budget"] not in budgets
or any(i not in itfs for i in v.get("interfaces") or [])):
rep.error(f"allocation: {k} names an unknown budget or interface")
ids = [r[0] for r in REQS]
if not ids:
return rep.warn("no requirement ids found in INPUT/requirements.md; coverage not checked")
missing = [i for i in ids if not allocation_for(alloc, i)]
if missing:
(rep.warn if sketch else rep.error)(f"allocation: {len(missing)} id(s) not allocated: {', '.join(missing)}")
for k in alloc:
if any(ch in str(k) for ch in "*?["):
if not any(fnmatch.fnmatchcase(i, str(k)) for i in ids):
rep.warn(f"allocation: pattern {k} matches no requirement id")
elif k not in ids and not str(k).startswith("DR-"):
rep.warn(f"allocation: {k} is not an id of INPUT/requirements.md (derived ids start with DR-)")
sys_only = [i for i in ids if allocation_for(alloc, i) == ["SYS"]]
if len(sys_only) > 0.4 * len(ids):
rep.warn(f"allocation: {len(sys_only)}/{len(ids)} ids only on SYS; give design requirements to subsystems")
def contract_level(reg, rep, sketch=False):
subs = check_subsystems(reg, rep)
check_interfaces(reg, rep, subs, sketch)
if not sketch or reg.get("envelopes"):
check_envelopes(reg, rep, subs)
check_budgets(reg, rep, subs, sketch)
check_allocation(reg, rep, subs, sketch)
return subs
# ------------------------------------------------------------------------------------------ DSM and diffs
def dsm(reg, subs):
"""m[i][j] = interface params through which subsystem i consumes from subsystem j."""
m = {i: {j: 0 for j in subs} for i in subs}
for itf in (x for x in reg.get("interfaces") or [] if isinstance(x, dict)):
for c in cons_of(itf):
if c in m and itf.get("provider") in m and c != itf.get("provider"):
m[c][itf["provider"]] += max(1, len(itf.get("params") or []))
return m
def sccs(subs, m):
"""Strongly connected components in provider-first order (Tarjan emits sinks first)."""
graph = {j: [i for i in subs if m[i][j] > 0] for j in subs}
index, low, stack, out = {}, {}, [], []
def visit(v):
index[v] = low[v] = len(index)
stack.append(v)
for w in graph[v]:
if w not in index:
visit(w)
low[v] = min(low[v], low[w])
elif w in stack:
low[v] = min(low[v], index[w])
if low[v] == index[v]:
comp = []
while not comp or comp[-1] != v:
comp.append(stack.pop())
out.append(sorted(comp, key=subs.index))
for v in subs:
if v not in index:
visit(v)
return out[::-1]
def coupling(reg, subs):
m, alloc = dsm(reg, subs), reg.get("allocation") or {}
internal = [i for i in reg.get("interfaces") or [] if isinstance(i, dict) and i.get("provider") in subs
and any(c in subs for c in cons_of(i))]
links = sum(1 for i in subs for j in subs if m[i][j] > 0)
shared = sum(1 for r in REQS if len([t for t in allocation_for(alloc, r[0]) if t in subs]) >= 2)
return {"interfaces": len(internal), "links": links, "params": sum(len(i.get("params") or []) for i in internal),
"blocks": [c for c in sccs(subs, m) if len(c) > 1], "shared": shared, "index": links + shared}
def affected_parties(old, new):
"""Internal subsystems whose part of the contract differs between two registries."""
def by(lst, key):
return {str(x.get(key)): x for x in (lst or []) if isinstance(x, dict)}
aff = set()
for sec, key in (("subsystems", "id"), ("envelopes", "subsystem")):
a, b = by(old.get(sec), key), by(new.get(sec), key)
aff |= {k for k in set(a) | set(b) if a.get(k) != b.get(k)}
a, b = by(old.get("interfaces"), "id"), by(new.get("interfaces"), "id")
for k in (k for k in set(a) | set(b) if a.get(k) != b.get(k)):
aff |= {str(x) for itf in (a.get(k), b.get(k)) if itf for x in parties(itf) if x}
if old.get("product_envelope_mm") != new.get("product_envelope_mm"):
aff |= {k for k, e in by(new.get("envelopes"), "subsystem").items() if e.get("shell")}
a, b = by(old.get("budgets"), "name"), by(new.get("budgets"), "name")
for k in set(a) | set(b):
x, y = a.get(k) or {}, b.get(k) or {}
xa, ya = x.get("allocations") or {}, y.get("allocations") or {}
if not x or not y or any(x.get(f) != y.get(f) for f in ("unit", "combine")):
aff |= set(xa) | set(ya)
else:
aff |= {s for s in set(xa) | set(ya) if xa.get(s) != ya.get(s)}
a, b = old.get("allocation") or {}, new.get("allocation") or {}
for k in (k for k in set(a) | set(b) if a.get(k) != b.get(k)):
aff |= set(alloc_targets(a.get(k))) | set(alloc_targets(b.get(k)))
return {x for x in aff if x and x != "SYS" and not is_ext(x)}
# ------------------------------------------------------------------------------------------- conformance
def load_conf(subdir, sid, rep):
data = read_yaml(os.path.join(subdir, sid, "conformance.yaml"), rep, sid)
if data is not None and not isinstance(data, dict):
rep.error(f"{sid}: conformance.yaml must be a mapping", sid)
return None
return data
def load_all_conf(reg, subdir, rep):
return {s: c for s in sub_ids(reg) + ["SYS"] for c in [load_conf(subdir, s, rep)] if c is not None}
def actual_value(a, b, rep=None, sid=None):
"""A budget actual ({value, unit, origin} or a bare number) converted to the budget's unit."""
a = a if isinstance(a, dict) else {"value": a}
v = num(a.get("value"))
if v is None:
return rep and rep.error(f"{sid}: {b['name']} actual {a.get('value')!r} is not a number", sid)
if rep and not a.get("origin"):
rep.warn(f"{sid}: {b['name']} actual has no origin", sid)
try:
return convert(v, a.get("unit", b.get("unit")), b.get("unit"))
except ValueError as exc:
return rep and rep.error(f"{sid}: budget {b['name']}: {exc}", sid)
def check_obligation(sid, itf, p, o, rep):
name, rule, unit, val = f"{itf['id']}.{p['name']}", p.get("rule", "within"), p.get("unit"), o.get("value")
if not o.get("origin"):
rep.warn(f"{sid}: {name} has no origin", sid)
if rule == "equals":
exp = p.get("value", p.get("pins"))
if norm(val) != norm(exp):
rep.error(f"{sid}: {name} = {val!r} does not match the contract value {exp!r}", sid)
return
rng = as_range(val)
if rng is None:
return rep.error(f"{sid}: {name} value {val!r} is not a number or [lo, hi]", sid)
try:
lo_v, hi_v = (convert(x, o.get("unit", unit), unit) for x in rng)
except ValueError as exc:
return rep.error(f"{sid}: {name}: {exc}", sid)
lo, hi = bounds(p)
eps = 1e-9 * max(1.0, abs(lo or 0), abs(hi or 0))
shown = fmt(lo_v) if lo_v == hi_v else f"[{fmt(lo_v)}, {fmt(hi_v)}]"
if rule == "within" and (lo is not None and lo_v < lo - eps or hi is not None and hi_v > hi + eps):
rep.error(f"{sid}: {name} = {shown} {unit} lies outside the contract {bounds_txt(p)} {unit}", sid)
if rule == "covers" and (lo is not None and lo_v > lo + eps or hi is not None and hi_v < hi - eps):
rep.error(f"{sid}: {name} tolerates {shown} {unit} but must cover the contract {bounds_txt(p)} {unit}", sid)
def check_req_entries(reg, rep, sid, c, strict=False):
"""Requirement entries of one conformance file. strict (release, owner self-check): an allocated id without a
status, or still not_addressed, is an ERROR."""
owner, seen, alloc = (sid if sid != "SYS" else None), {}, reg.get("allocation") or {}
for r in c.get("requirements") or []:
if not isinstance(r, dict) or not r.get("id"):
rep.warn(f"{sid}: requirement entry without id: {str(r)[:60]}", owner)
continue
if r.get("status") not in STATUSES:
rep.error(f"{sid}: requirement {r['id']} status {r.get('status')!r} not in {STATUSES}", owner)
for ref in (x for x in re.split(r"[;|]", str(r.get("evidence") or "")) if x.strip()):
if not evidence_path(ref, sid):
rep.warn(f"{sid}: {r['id']} evidence not found: {ref.strip()}", owner)
seen[r["id"]] = r.get("status")
mine = [r[0] for r in REQS if sid in allocation_for(alloc, r[0])]
missing = [i for i in mine if i not in seen]
still_open = [i for i in mine if seen.get(i) == "not_addressed"]
if owner and (missing or still_open):
(rep.error if strict else rep.warn)(
f"{sid}: allocated requirement(s) without a status: {', '.join(missing) or '-'}; still not_addressed: "
f"{', '.join(still_open) or '-'} - give each met / partly / not_met with the predicted value", sid)
def check_conf(reg, rep, sid, c, version, regdir, strict=False):
if c.get("subsystem") != sid:
rep.error(f"{sid}: conformance.yaml says subsystem {c.get('subsystem')!r}", sid)
cv = c.get("contract_version")
if not isinstance(cv, int) or not 1 <= cv <= version:
rep.error(f"{sid}: contract_version {cv!r} invalid (contract is v{version})", sid)
elif cv < version:
old = load_snapshot(regdir, cv)
if old is None:
rep.warn(f"{sid}: conformance is against v{cv}; no snapshot to tell whether it is stale", sid)
elif sid in affected_parties(old, reg):
rep.error(f"{sid}: STALE - conformance is against v{cv} but v{version} changed {sid}'s part of the "
f"contract; update the values and set contract_version: {version}", sid)
else:
rep.note(f"{sid}: conformance against v{cv} still valid at v{version} (nothing of {sid} changed)")
obl = {(str(o.get("interface")), str(o.get("param"))): o for o in c.get("obligations") or [] if isinstance(o, dict)}
expected = set()
for itf in reg.get("interfaces") or []:
for p in (p for p in itf.get("params") or [] if isinstance(p, dict) and sid in param_party(itf, p)):
key = (str(itf.get("id")), str(p.get("name")))
expected.add(key)
if key in obl:
check_obligation(sid, itf, p, obl[key], rep)
else:
rep.error(f"{sid}: obligation {key[0]}.{key[1]} not reported in conformance.yaml", sid)
for key in (k for k in obl if k not in expected):
rep.warn(f"{sid}: reports {key[0]}.{key[1]}, which is not an obligation of {sid}", sid)
actual = c.get("budgets") or {}
for b in (b for b in reg.get("budgets") or [] if sid in (b.get("allocations") or {})):
if b["name"] not in actual:
rep.error(f"{sid}: budget actual '{b['name']}' missing", sid)
continue
v, cap = actual_value(actual[b["name"]], b, rep, sid), num(b["allocations"][sid])
if v is not None and cap is not None and v > cap + 1e-9 * max(1.0, abs(cap)):
rep.error(f"{sid}: {b['name']} actual {fmt(v)} {b.get('unit')} exceeds its allocation {fmt(cap)} "
f"{b.get('unit')} - redesign or propose an amendment", sid)
check_req_entries(reg, rep, sid, c, strict)
pids = [p.get("id") for p in c.get("principles") or [] if isinstance(p, dict)]
if len(pids) < 2:
rep.warn(f"{sid}: {len(pids)} principle(s) compared; the method asks for 2", sid)
elif c.get("principle") not in pids:
rep.warn(f"{sid}: principle {c.get('principle')!r} is not one of principles[].id", sid)
def principle_actual(conf, pid, b):
"""Actual of budget b: the top-level budgets for the current principle, else principles[pid].budgets."""
if not conf:
return None
if pid in (None, conf.get("principle")) and b["name"] in (conf.get("budgets") or {}):
return actual_value(conf["budgets"][b["name"]], b)
for p in conf.get("principles") or []:
if isinstance(p, dict) and p.get("id") == (pid or conf.get("principle")) and b["name"] in (p.get("budgets") or {}):
return actual_value(p["budgets"][b["name"]], b)
return None
def rollup(reg, confs, choice=None):
"""[(budget, total, margin, pct, projected)]: actuals of the chosen principles, else the allocation."""
rows = []
for b in (b for b in reg.get("budgets") or [] if isinstance(b, dict) and b.get("name")):
vals, projected = {}, []
for s, a in (b.get("allocations") or {}).items():
v = principle_actual(confs.get(s), (choice or {}).get(s), b)
vals[s] = v if v is not None else num(a)
projected += [s] if v is None else []
rows.append((b, *closure(b, vals), projected))
return rows
def evidence_path(ref, sid):
ref = str(ref).strip().strip("`'\"").lstrip("./")
ref = ref[len(DELIV) + 1:] if ref.startswith(DELIV + "/") else ref
for cand in (ref, f"02-concept/subsystems/{sid}/{ref}", f"02-concept/{ref}"):
if os.path.exists(os.path.join(DELIV, cand.split("#")[0])):
return cand
return None
# -------------------------------------------------------------------------------------------- amendments
def find_amd(regdir, aid):
path = os.path.join(regdir, "amendments", f"{aid}.yaml")
data = read_yaml(path)
if not isinstance(data, dict):
sys.exit(f"contract_check: {path} missing or not a mapping")
return path, data
def _match(item, seg):
"""A list item is named by its id, name or subsystem (mappings) or by its value (plain values)."""
if isinstance(item, dict):
return seg in (str(item.get("id")), str(item.get("name")), str(item.get("subsystem")))
return str(item) == seg
def _key_of(item):
if isinstance(item, dict):
return next((str(item[k]) for k in ("id", "name", "subsystem") if item.get(k) is not None), None)
return str(item)
def _item(node, seg):
if isinstance(node, list):
return next((i for i in node if isinstance(i, dict) and _match(i, seg)), None)
return node.get(seg) if isinstance(node, dict) else None
def apply_change(reg, ch):
"""One change of an amendment. where = <section>/<id|name|subsystem>/.../<field>; list items are found by id,
name or subsystem (plain values by value).
op: set (default) - 'new' merges into a mapping (a null value deletes that key) or replaces the value
op: add - 'where' names the list or mapping that receives 'new': a list gets the item (or each item of a
list) appended, a mapping gets the keys of 'new'; what exists already is refused (use set)
op: remove - 'where' names the list item or mapping key to delete; it must exist ('new' is not used)"""
op = str(ch.get("op") or "set").strip().lower()
path = [p for p in str(ch.get("where", "")).split("/") if p]
if op not in ("set", "add", "remove"):
raise ValueError(f"op '{op}' unknown (set | add | remove)")
if not path or (op != "remove" and "new" not in ch):
raise ValueError("needs 'where'" + ("" if op == "remove" else " and 'new'"))
node = reg
for seg in path[:-1]:
node = _item(node, seg)
if node is None:
raise KeyError(f"'{seg}' not found")
last, new = path[-1], ch.get("new")
if op == "remove":
idx = next((i for i, x in enumerate(node) if _match(x, last)), None) if isinstance(node, list) else None
if idx is not None:
node.pop(idx)
elif isinstance(node, dict) and last in node:
node.pop(last)
else:
raise KeyError(f"'{last}' not found: nothing to remove")
return
target = _item(node, last) if isinstance(node, list) else node.get(last) if isinstance(node, dict) else None
if op == "add":
if target is None:
if not isinstance(node, dict):
raise KeyError(f"'{last}' not found: for op add, 'where' names the list or mapping that receives "
"'new' (e.g. interfaces/IF-01/params)")
node[last] = (list(new) if isinstance(new, list) else [new]) if last in LIST_KEYS else new
elif isinstance(target, list):
for item in new if isinstance(new, list) else [new]:
key = _key_of(item)
if key is None or any(_match(x, key) for x in target):
raise ValueError(f"'{last}' already has an item '{key}' (change it with op: set)" if key else
f"a new item of '{last}' needs an id, name or subsystem")
target.append(item)
elif isinstance(target, dict) and isinstance(new, dict):
dup = [str(k) for k in new if k in target]
if dup:
raise ValueError(f"'{last}' already has {', '.join(dup)} (change it with op: set)")
target.update(new)
else:
raise ValueError(f"'{last}' is a single value or 'new' does not fit it: use op: set")
return
if isinstance(node, list) and target is None:
raise KeyError(f"no list item '{last}' (a new item: op: add on the list)")
if isinstance(target, dict) and isinstance(new, dict):
for k, v in new.items():
target.pop(k, None) if v is None else target.__setitem__(k, v)
elif isinstance(node, dict):
node[last] = new
else:
raise ValueError(f"'{last}' is a list item: give a mapping in 'new'")
def apply_amendment(reg, amd, regdir):
new, errs = copy.deepcopy(reg), []
if amd.get("proposed_registry"):
d = os.path.join(regdir, "amendments", str(amd["proposed_registry"]))
found = [sections_of(read_yaml(os.path.join(d, n + ".yaml")) or {}) for n in SECTIONS]
if not any(found):
errs.append(f"proposed_registry {d} holds no interfaces/budgets/allocation.yaml")
for f in found:
new.update(f)
for ch in amd.get("changes") or []:
try:
apply_change(new, ch if isinstance(ch, dict) else {})
except (KeyError, ValueError, TypeError, AttributeError) as exc:
errs.append(f"change {ch.get('where') if isinstance(ch, dict) else ch!r}: {exc}")
if not amd.get("proposed_registry") and not amd.get("changes"):
errs.append("no 'changes' and no 'proposed_registry'")
return new, errs
def check_amendments(regdir, reg, subs, rep, release):
for path in sorted(glob.glob(os.path.join(regdir, "amendments", "*.yaml"))):
amd = read_yaml(path, rep)
if not isinstance(amd, dict):
continue
aid, pb, st = amd.get("id"), amd.get("proposed_by"), amd.get("status", "proposed")
owner = pb if pb in subs else None
if aid != os.path.basename(path)[:-5] or (pb not in subs and pb != "SYS") or st not in AMD_STATUS:
rep.error(f"{path}: id must equal the file name, proposed_by a subsystem id or SYS, status one of "
f"{AMD_STATUS}", owner)
if st in ("proposed", "routed", "accepted"):
(rep.error if release else rep.warn)(f"{aid}: open amendment (status {st})", owner)
for e in apply_amendment(reg, amd, regdir)[1]:
rep.error(f"{aid}: {e}", owner)
# --------------------------------------------------------------------------------------- freeze and views
def write_views(regdir, reg, subs, version):
m = dsm(reg, subs)
comps = sccs(subs, m)
order, blocks = [s for c in comps for s in c], [c for c in comps if len(c) > 1]
with open(os.path.join(regdir, "dsm.csv"), "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["row depends on column (interface params)"] + order)
w.writerows([i] + ["-" if i == j else m[i][j] for j in order] for i in order)
env = {e.get("subsystem"): e for e in reg.get("envelopes") or [] if isinstance(e, dict)}
alloc = reg.get("allocation") or {}
out = [f"# Contract registry v{version}", "", "View generated by `contract_check.py --freeze`; the YAML files are "
"the source.", "", f"Frame: {reg.get('frame', '-')} ",
f"Product envelope (mm): {reg.get('product_envelope_mm', '-')}", "", "## Subsystems", "",
"| id | name | scope | envelope (mm) |", "|---|---|---|---|"]
out += [f"| {s.get('id')} | {s.get('name', '')} | {s.get('scope', '')} | "
f"{('shell ' if env.get(s.get('id'), {}).get('shell') else '') + str(boxes_of(env.get(s.get('id'))))} |"
for s in reg.get("subsystems") or []]
out += ["", "## Interfaces", "", "| interface | kind | provider -> consumers | param | rule | bounds | unit | "
"must satisfy | origin |", "|---|---|---|---|---|---|---|---|---|"]
for itf in reg.get("interfaces") or []:
head = f"| {itf.get('id')} {itf.get('title', '')} | {itf.get('kind')} | {itf.get('provider')} -> " \
f"{', '.join(cons_of(itf))} |"
out += [head + f" {p.get('name')} | {p.get('rule', 'within')} | {bounds_txt(p)} | {p.get('unit', '')} | "
f"{', '.join(param_party(itf, p))} | {p.get('origin', '')} |" for p in itf.get("params") or []] \
or [head + " - | | | | | |"]
out += ["", "## Budgets", "", "| budget | unit | limit | goal | reserve | combine | allocations | combined |",
"|---|---|---|---|---|---|---|---|"]
out += [f"| {b.get('name')} | {b.get('unit')} | {b.get('limit')} | {b.get('goal', '-')} | {fmt(reserve_of(b))} | "
f"{b.get('combine', 'sum')} | {', '.join(f'{k}: {v}' for k, v in (b.get('allocations') or {}).items())} | "
f"{fmt(combine(b.get('combine', 'sum'), [num(v) for v in (b.get('allocations') or {}).values()]))} |"
for b in reg.get("budgets") or []]
out += ["", "## Requirement allocation", "", "| requirement | title | priority | allocated to |", "|---|---|---|---|"]
out += [f"| {r} | {t} | {fmt(p) if p else '-'} | {', '.join(allocation_for(alloc, r)) or 'NOT ALLOCATED'} |"
for r, t, p in REQS]
out += ["", "## DSM (row depends on column; numbers = interface params; providers first)", "",
"| | " + " | ".join(order) + " |", "|---" * (len(order) + 1) + "|"]
out += [f"| {i} | " + " | ".join("-" if i == j else str(m[i][j]) for j in order) + " |" for i in order]
out += ["", "Coupled blocks (fix their shared parameters first): "
+ ("; ".join(" <-> ".join(b) for b in blocks) or "none"), ""]
with open(os.path.join(regdir, "REGISTRY.md"), "w", encoding="utf-8") as fh:
fh.write("\n".join(out))
exts = sorted({x for i in reg.get("interfaces") or [] for x in parties(i) if is_ext(x)})
edges = [(str(i.get("provider")), str(c), str(i.get("id")), i.get("kind", "")) for i in reg.get("interfaces") or []
for c in cons_of(i)]
def nid(x):
return re.sub(r"\W", "_", str(x))
mmd = ["flowchart LR"] + [f' {nid(s["id"])}["{s["id"]}<br/>{str(s.get("name", "")).replace(chr(34), "")}"]'
for s in reg.get("subsystems") or []]
mmd += [f' {nid(x)}(["{x[4:]}"])' for x in exts] + [f" {nid(a)} -->|{i} {k}| {nid(b)}" for a, b, i, k in edges]
with open(os.path.join(regdir, "architecture.mmd"), "w", encoding="utf-8") as fh:
fh.write("\n".join(mmd) + "\n")
try: # a rendered picture of the same graph; optional, the .mmd is the record
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import networkx as nx
g, labels = nx.DiGraph(), {}
g.add_nodes_from(subs + exts)
for a, b, i, _ in edges:
g.add_edge(a, b)
labels[(a, b)] = (labels.get((a, b), "") + " " + i).strip()
pos, (fig, ax) = nx.circular_layout(g), plt.subplots(figsize=(9, 7))
nx.draw_networkx_nodes(g, pos, ax=ax, node_size=2600,
node_color=["#cfe3f5" if n in subs else "#eeeeee" for n in g.nodes])
nx.draw_networkx_labels(g, pos, ax=ax, font_size=8)
nx.draw_networkx_edges(g, pos, ax=ax, arrowsize=14, node_size=2600, connectionstyle="arc3,rad=0.15")
nx.draw_networkx_edge_labels(g, pos, edge_labels=labels, ax=ax, font_size=7, connectionstyle="arc3,rad=0.15")
ax.set_title(f"Architecture from contract v{version} (arrow: provider -> consumer)")
ax.axis("off")
fig.savefig(os.path.join(regdir, "architecture.png"), dpi=110, bbox_inches="tight")
plt.close(fig)
except Exception: # noqa: BLE001
pass
return blocks
def do_freeze(regdir, reg, subs, note):
v = read_version(regdir) + 1
snap = os.path.join(regdir, "history", f"v{v}")
os.makedirs(snap, exist_ok=True)
for name in SECTIONS:
shutil.copy2(os.path.join(regdir, name + ".yaml"), snap)
with open(os.path.join(regdir, "VERSION"), "w") as fh:
fh.write(f"{v}\n")
write_views(regdir, reg, subs, v)
with open(os.path.join(regdir, "CHANGELOG.md"), "a", encoding="utf-8") as fh:
fh.write(f"- v{v} ({now()}): {note}\n")
return v
# ------------------------------------------------------------------------------------------------- modes
def mode_check(regdir, subdir, rep, release):
reg = load_registry(regdir, rep)
if not reg:
return rep.focus and rep.error(f"no contract registry in {os.path.abspath(regdir)}: nothing to check "
"against", rep.focus)
version, subs = read_version(regdir), contract_level(reg, rep)
if rep.focus and rep.focus not in subs:
rep.error(f"'{rep.focus}' is not a subsystem id of the contract ({', '.join(subs)})", rep.focus)
if version < 1:
(rep.error if rep.focus else rep.warn)("contract not frozen yet (no VERSION): "
+ ("nothing to check against" if rep.focus else
"run --freeze when it is complete"), rep.focus)
snap = load_snapshot(regdir, version) if version else None
edits = sorted(k for k in set(reg) | set(snap or {}) if snap is not None and reg.get(k) != snap.get(k))
if edits:
(rep.error if release else rep.warn)(f"registry differs from frozen v{version} in {', '.join(edits)}: "
"change it only with --freeze (before owners) or --merge")
confs = {}
if version >= 1:
for sid in subs + ["SYS"]:
c = load_conf(subdir, sid, rep)
strict = release or rep.focus == sid
if c is None and sid != "SYS":
(rep.error if strict else rep.warn)(f"{sid}: no conformance.yaml", sid)
elif c is not None:
confs[sid] = c
if sid != "SYS":
check_conf(reg, rep, sid, c, version, regdir, strict)
else:
check_req_entries(reg, rep, sid, c)
alloc = reg.get("allocation") or {}
unresolved = [r[0] for r in REQS if not any( # the entries --compliance uses: allocated subsystems and SYS
isinstance(e, dict) and e.get("id") == r[0] and e.get("status") in STATUSES[:3]
for s in dict.fromkeys(allocation_for(alloc, r[0]) + ["SYS"])
for e in (confs.get(s) or {}).get("requirements") or [])]
if unresolved:
(rep.error if release else rep.warn)(
f"{len(unresolved)} requirement id(s) without met / partly / not_met in any conformance file: "
f"{', '.join(unresolved)} (owners: FIX; system-level ids: subsystems/SYS/conformance.yaml)")
for b, tot, margin, pct, proj in rollup(reg, confs):
unit, goal = b.get("unit"), num(b.get("goal"))
gpct = None if goal in (None, 0) or tot is None or not pct_ok(b) else 100 * (goal - tot) / abs(goal)
gtxt = "" if goal is None or tot is None else f"; vs goal {fmt(goal)}: {margin_txt(goal - tot, gpct, unit)}"
rep.note(f"budget {b['name']}: {fmt(tot)} {unit} vs limit {b.get('limit')} -> margin "
f"{margin_txt(margin, pct, unit)}{gtxt}" + (f" (allocation used for {', '.join(proj)})" if proj else ""))
if over(margin, b):
rep.error(f"SYSTEM BUDGET VIOLATION {b['name']}: {fmt(tot)} > limit {b.get('limit')} {unit} - the "
"architect decides (method/POLICY.md)")
check_amendments(regdir, reg, subs, rep, release)
rep.note(f"contract v{version}; subsystems {', '.join(subs)}; conformance: {', '.join(confs) or 'none'}")
def mode_candidates(regdir, rep):
rows = []
for f in sorted(glob.glob(os.path.join(regdir, "candidates", "*.yaml"))):
name, reg, crep = os.path.basename(f)[:-5], sections_of(read_yaml(f, rep) or {}), Report()
subs = contract_level(reg, crep, sketch=True)
for e in crep.errors:
rep.error(f"{name}: {e}")
cp, margins, ko, pcts = coupling(reg, subs), [], [], []
for b in (b for b in reg.get("budgets") or [] if isinstance(b, dict)):
src = b.get("estimates") or b.get("allocations") or {}
tot, margin, pct = closure(b, {s: num(v) for s, v in src.items()})
margins.append(f"{b.get('name')} {margin_txt(margin, pct, b.get('unit'))}")
ko += [str(b.get("name"))] if over(margin, b) else []
pcts += [pct] if pct is not None else []
rows.append((name, subs, cp, margins, ko, min(pcts) if pcts else -1e9))
if not rows:
return rep.error(f"no candidate files in {os.path.join(regdir, 'candidates')}/")
out = ["| candidate | subsystems | internal interfaces | links | params | coupled blocks | shared req ids | "
"coupling index | budget margins | min margin | knock-out |", "|---|---|---|---|---|---|---|---|---|---|---|"]
out += [f"| {n} | {len(s)}: {', '.join(s)} | {c['interfaces']} | {c['links']} | {c['params']} | "
f"{'; '.join('<->'.join(b) for b in c['blocks']) or '-'} | {c['shared']} | {c['index']} | "
f"{'; '.join(mg)} | {min_txt(mn)} | {'KO: ' + ', '.join(ko) if ko else '-'} |"
for n, s, c, mg, ko, mn in rows]
alive = [r for r in rows if not r[4]] or sorted(rows, key=lambda r: len(r[4]))[:1]
top = max(alive, key=lambda r: r[5])
close = [r for r in alive if r is not top and r[5] >= top[5] - 5 and r[2]["index"] <= 0.75 * top[2]["index"]]
best = min(close, key=lambda r: r[2]["index"]) if close else top
out += ["", f"Suggested by the POLICY rule (knock-out on budget closure, then the largest minimum margin, unless "
f"another survivor couples >= 25 % less within 5 margin points; margins in % of the limit, temperature "
f"caps and dB budgets do not enter the minimum): **{best[0]}**. Record the decision and why the others "
"lost in 02-concept/partitions.md.", ""]
print("\n".join(out))
with open(os.path.join(regdir, "candidates", "metrics.md"), "w", encoding="utf-8") as fh:
fh.write("# Candidate partitions (generated by contract_check.py --candidates)\n\n" + "\n".join(out))
def mode_freeze(regdir, subdir, note, rep):
reg = load_registry(regdir, rep)
subs = contract_level(reg, rep) if not rep.errors else []
started = [s for s in subs if os.path.isfile(os.path.join(subdir, s, "conformance.yaml"))]
if started and read_version(regdir) >= 1:
rep.error(f"owners already conform ({', '.join(started)}): change the contract with an amendment "
"(AMD-SYS-nn, then --route and --merge)")
if rep.errors:
return rep.error("not frozen: fix the errors above")
rep.note(f"frozen as v{do_freeze(regdir, reg, subs, note)}: history/, dsm.csv, REGISTRY.md, architecture.mmd")
def mode_brief(regdir, sid, rep):
reg = load_registry(regdir, rep)
subs, v, alloc = sub_ids(reg), read_version(regdir), reg.get("allocation") or {}
if sid not in subs:
return rep.error(f"'{sid}' is not a subsystem id ({', '.join(subs)})")
s = next(x for x in reg["subsystems"] if str(x.get("id")) == sid)
env = next((e for e in reg.get("envelopes") or [] if isinstance(e, dict) and e.get("subsystem") == sid), None)
out = [f"# Contract view for {sid} - contract v{v}", "", f"Name: {s.get('name', '')}", f"Scope: {s.get('scope', '')}",
f"Principles to compare (architect's hint): {s.get('principles', '-')}",
f"Firmware: {'you own 07-firmware/' if s.get('owns_firmware') else 'not yours'}",
f"Frame: {reg.get('frame', '-')}", f"Envelope boxes (mm, product frame): {boxes_of(env) if env else 'none'}"
+ (" - shell: may enclose the other envelopes" if env and env.get("shell") else ""), "",
"## Obligations: report each in conformance.yaml -> obligations", "",
"| interface | param | rule | bounds | unit | other parties | origin |", "|---|---|---|---|---|---|---|"]
obligations, consumed, budgets, reqs = [], [], {}, []
for itf in reg.get("interfaces") or []:
for p in itf.get("params") or []:
if sid in param_party(itf, p):
obligations.append({"interface": itf.get("id"), "param": p.get("name"), "value": None,
"unit": p.get("unit", ""), "origin": ""})
out.append(f"| {itf.get('id')} {itf.get('title', '')} | {p.get('name')} | {p.get('rule', 'within')} | "
f"{bounds_txt(p)} | {p.get('unit', '')} | {', '.join(x for x in parties(itf) if x != sid)} | "
f"{p.get('origin', '')} |")
elif sid in parties(itf):
consumed.append(f"| {itf.get('id')} {itf.get('title', '')} | {p.get('name')} | {bounds_txt(p)} | "
f"{p.get('unit', '')} | {', '.join(param_party(itf, p))} |")
out += ["", "## Guaranteed to you by others: design for the whole range", "",
"| interface | param | bounds | unit | guaranteed by |", "|---|---|---|---|---|"] + (consumed or ["| - |"])
out += ["", "## Budgets: report actuals in conformance.yaml -> budgets", "",
"| budget | unit | your allocation | system limit | combine |", "|---|---|---|---|---|"]
for b in (b for b in reg.get("budgets") or [] if sid in (b.get("allocations") or {})):
budgets[b["name"]] = {"value": None, "unit": b.get("unit"), "origin": ""}
out.append(f"| {b['name']} | {b.get('unit')} | {b['allocations'][sid]} | {b.get('limit')} | "
f"{b.get('combine', 'sum')} |")
out += ["", "## Requirements allocated to you: report each in conformance.yaml -> requirements", ""]
for rid, title, prio in REQS:
t = allocation_for(alloc, rid)
if sid in t:
reqs.append({"id": rid, "status": "not_addressed", "value": "", "unit": "", "evidence": "", "note": ""})
out.append(f"- {rid} - {title}" + (f" (priority {fmt(prio)})" if prio else "")
+ (f" [shared with {', '.join(x for x in t if x != sid)}]" if len(t) > 1 else ""))
m = dsm(reg, subs)
out += ["", f"DSM: {sid} depends on " + (", ".join(f"{j} ({m[sid][j]})" for j in subs if m[sid][j]) or "nobody")
+ "; depend on it: " + (", ".join(f"{i} ({m[i][sid]})" for i in subs if m[i][sid]) or "nobody"), "",
f"## Conformance skeleton for deliverables/02-concept/subsystems/{sid}/conformance.yaml", "", "```yaml"]
pr = [{"id": x, "name": "", "summary": "", "verdict": vd, "reason": "", "budgets": copy.deepcopy(budgets)}
for x, vd in (("A", "recommended"), ("B", "rejected"))]
skel = {"subsystem": sid, "contract_version": v, "principle": "A", "principles": pr, "obligations": obligations,
"budgets": budgets, "requirements": reqs, "amendments": []}
out += [yaml.safe_dump(skel, sort_keys=False, allow_unicode=True, width=110, default_flow_style=None).rstrip(), "```"]
print("\n".join(out))
def mode_route(regdir, aid, rep):
path, amd = find_amd(regdir, aid)
reg = load_registry(regdir, rep)
if amd.get("status", "proposed") not in ("proposed", "routed"):
return rep.error(f"{aid} has status {amd.get('status')}; only proposed or routed amendments are routed")
new, errs = apply_amendment(reg, amd, regdir)
trep = Report()
if not errs:
contract_level(new, trep)
for e in errs + [f"after the change: {x}" for x in trep.errors]:
rep.error(f"{aid}: {e}")
if rep.errors:
return rep.error(f"{aid} cannot be routed: send the errors to {amd.get('proposed_by')} or --reject it")
affected = sorted(affected_parties(reg, new))
voters = [s for s in affected if s != amd.get("proposed_by")]
amd.update(affected=affected, voters=voters, status="routed" if voters else "accepted")
dump_yaml(path, amd)
if not voters:
return rep.note(f"{aid} affects only {amd.get('proposed_by')}: status accepted, run --merge {aid}")
print(f"Send to each of {', '.join(voters)} (SendMessage):\n{aid} needs your vote (contract v{read_version(regdir)}): "
f"{amd.get('summary', '')}. Read {path}. Reply by SendMessage to main with one line: '{aid} ACCEPT: <reason>' "
f"or '{aid} REJECT: <the number that breaks>; counter-proposal: <...>'.\n")
rep.note(f"{aid} routed to {', '.join(voters)}")
def mode_vote(regdir, aid, sid, verdict, reason, rep):
path, amd = find_amd(regdir, aid)
verdict = verdict.upper()
if verdict not in ("ACCEPT", "REJECT"):
return rep.error("verdict must be ACCEPT or REJECT")
if sid not in (amd.get("voters") or []):
rep.warn(f"{sid} is not among the voters {amd.get('voters')} (recorded anyway)")
amd.setdefault("votes", {})[sid] = {"verdict": verdict, "reason": reason, "at": now()}
if verdict == "REJECT":
amd.update(status="rejected", decision={"status": "rejected", "at": now(), "note": f"{sid}: {reason}"})
rep.note(f"{aid} rejected by {sid}: tell {amd.get('proposed_by')} the reason")
elif all((amd["votes"].get(x) or {}).get("verdict") == "ACCEPT" for x in amd.get("voters") or []):
amd["status"] = "accepted"
rep.note(f"{aid}: every voter accepted; run --merge {aid}")
dump_yaml(path, amd)
def sbv_refusal(old, new, viol):
"""Why an AMD-SYS with basis system_budget_violation may NOT skip the votes (None = it may). POLICY section 2:
the architect alone may use the reserve and change a derived limit of a violated budget; taking allocation
from an owner (step 2) goes through the normal route."""
if any(old.get(k) != new.get(k) for k in set(old) | set(new) if k != "budgets"):
return "it changes more than budgets: route it (--route)"
a = {b.get("name"): b for b in old.get("budgets") or [] if isinstance(b, dict)}
z = {b.get("name"): b for b in new.get("budgets") or [] if isinstance(b, dict)}
changed = {n for n in set(a) | set(z) if a.get(n) != z.get(n)}
if not changed & viol:
return f"none of its budgets is violated now (violated: {sorted(viol) or 'none'})"
if changed - viol:
return f"it also changes budgets that are not violated ({', '.join(sorted(map(str, changed - viol)))}): route it"
lowered = sorted(f"{s} ({n} {v} -> {(z[n].get('allocations') or {}).get(s)})" for n in changed if n in a and n in z
for s, v in (a[n].get("allocations") or {}).items() if s != "SYS" and num(v) is not None
and (num((z[n].get("allocations") or {}).get(s)) or 0.0) < num(v) - 1e-9 * max(1.0, abs(num(v))))
if lowered:
return (f"it lowers the allocation of {', '.join(lowered)}: POLICY section 2 step 2 needs that owner's vote - "
"route it (--route)")
return None
def mode_merge(regdir, subdir, aid, rep):
path, amd = find_amd(regdir, aid)
reg = load_registry(regdir, rep)
new, errs = apply_amendment(reg, amd, regdir)
if amd.get("status") != "accepted":
bad = "needs ACCEPT from every voter (run --route, then --vote)"
if amd.get("proposed_by") == "SYS" and amd.get("basis") == "system_budget_violation":
viol = {b["name"] for b, t, mg, p, pr in rollup(reg, load_all_conf(reg, subdir, Report())) if over(mg, b)}
bad = sbv_refusal(reg, new, viol)
bad = bad and f"basis system_budget_violation, but {bad}"
if bad:
return rep.error(f"{aid} (status {amd.get('status', 'proposed')}): {bad}")
trep = Report()
subs = contract_level(new, trep) if not errs else []
for e in errs + [f"after the change: {x}" for x in trep.errors]:
rep.error(f"{aid}: {e}")
if rep.errors:
return rep.error(f"{aid} not merged")
write_registry(regdir, new)
v = do_freeze(regdir, new, subs, f"{aid} merged: {amd.get('summary', '')}")
amd.setdefault("affected", sorted(affected_parties(reg, new)))
amd.update(status="merged", decision={"status": "merged", "version": v, "at": now()})
dump_yaml(path, amd)
rep.note(f"{aid} merged: contract v{v}. Tell {', '.join(amd['affected']) or 'the owners'} (SendMessage): "
f"'CONTRACT v{v} in force: {aid} merged - self-check and update conformance if STALE'")
def mode_reject(regdir, aid, reason, rep):
path, amd = find_amd(regdir, aid)
amd.update(status="rejected", decision={"status": "rejected", "at": now(), "note": reason})
dump_yaml(path, amd)
rep.note(f"{aid} rejected: {reason}")
def mode_alternatives(regdir, subdir, rep):
reg, path = load_registry(regdir, rep), os.path.join(regdir, "alternatives.yaml")
data = read_yaml(path, rep)
if not isinstance(data, dict) or not data.get("alternatives"):
return rep.error(f"{path} missing or without alternatives (format: method/templates/alternatives.yaml)")
confs = load_all_conf(reg, subdir, Report())
base = {c["name"]: num(c["weight"]) for c in data.get("criteria") or []
if isinstance(c, dict) and num(c.get("weight")) is not None}
res = []
for a in data["alternatives"]:
ko, cells, pcts = [str(a["knocked_out"])] if a.get("knocked_out") else [], [], []
for b, tot, margin, pct, proj in rollup(reg, confs, a.get("choice") or {}):
cells.append(f"{b['name']} {margin_txt(margin, pct, b.get('unit'))}")
pcts += [pct] if pct is not None else []
ko += [f"budget {b['name']} does not close"] if over(margin, b) else []
missing = [n for n in base if num((a.get("scores") or {}).get(n)) is None]
if missing:
rep.warn(f"{a.get('id')}: no score for {', '.join(missing)}")
res.append({**a, "ko": ko, "cells": cells, "min": min(pcts) if pcts else -1e9})
def ranking(weights):
"""[(score, alternative)] of the survivors, the winner by the POLICY rule first: the highest weighted
score, but among those within WINDOW points of it the larger minimum budget margin (then the score)."""
alive = [(sum(w * (num((r.get("scores") or {}).get(n)) or 0.0) for n, w in weights.items())
/ (sum(weights.values()) or 1.0), r) for r in res if not r["ko"]]
if not alive:
return []
top = max(s for s, _ in alive)
win = max((x for x in alive if x[0] >= top - WINDOW - 1e-9), key=lambda x: (x[1]["min"], round(x[0], 6)))
return [win] + sorted((x for x in alive if x is not win), key=lambda x: (round(x[0], 6), x[1]["min"]),
reverse=True)
ranked = ranking(base)
winner, scores = (ranked[0][1]["id"] if ranked else None), {r["id"]: s for s, r in ranked}
sens = [f"weight of '{n}' x{f} -> {r[0][1]['id']} wins" for n in base for f in (0.8, 1.2)
for r in [ranking({**base, n: base[n] * f})] if r and r[0][1]["id"] != winner]
out = ["| alternative | principles | budget margins | min margin | knock-out | weighted score (0-10) |",
"|---|---|---|---|---|---|"]
for r in res:
choice = ", ".join(f"{k}: {v}" for k, v in (r.get("choice") or {}).items()) or "recommended"
out.append(f"| {r.get('id')} {r.get('name', '')} | {choice} | {'; '.join(r['cells'])} | {min_txt(r['min'])} | "
f"{'; '.join(r['ko']) or '-'} | {fmt(scores[r['id']]) if r.get('id') in scores else 'KO'} |")
why = ""
if ranked:
best_s, best = max(((s, r) for s, r in ranked), key=lambda x: round(x[0], 6))
if best["id"] != winner:
why = (f" ({winner} scores {fmt(ranked[0][0])}, within {WINDOW} of {best['id']} with {fmt(best_s)}, and has "
f"the larger minimum budget margin: {min_txt(ranked[0][1]['min'])} vs {min_txt(best['min'])})")
out += ["", f"Winner by the POLICY rule (knock-outs; then the highest weighted score, but within {WINDOW} points of "
f"it the larger minimum budget margin in % of the limit, temperature caps and dB budgets excluded): "
f"**{winner or 'none - every alternative is knocked out'}**{why}. Sensitivity (each weight x0.8 and x1.2, "
"same rule): " + ("; ".join(sens) if sens else "the winner holds") + ".", ""]
print("\n".join(out))
with open(os.path.join(regdir, "alternatives.md"), "w", encoding="utf-8") as fh:
fh.write("# System alternatives (generated by contract_check.py --alternatives)\n\n" + "\n".join(out))
def budget_status(b, tot, margin, proj):
"""compliance entry of a requirement linked to budget b, from the system roll-up (None if nothing reported)."""
if tot is None or set(proj) >= {s for s in (b.get("allocations") or {}) if s != "SYS"}:
return None
goal, unit = num(b.get("goal")), b.get("unit", "")
st = "not_met" if over(margin, b) else \
"partly" if goal is not None and tot > goal + 1e-9 * max(1.0, abs(goal)) else "met"
note = (f"budget {b['name']}: {b.get('combine', 'sum')} of the conformance actuals {fmt(tot)} {unit} vs limit "
f"{b.get('limit')}" + (f", goal {fmt(goal)}" if goal is not None else "")
+ (f" (allocation used for {', '.join(proj)})" if proj else ""))
return {"status": st, "value": f"{tot:.6g}", "unit": unit, "note": note}
def mode_compliance(regdir, subdir, out_csv, rep):
reg = load_registry(regdir, rep)
confs, alloc, rank = load_all_conf(reg, subdir, rep), reg.get("allocation") or {}, ("not_met", "partly", "met")
rolled = {b["name"]: (b, tot, margin, proj) for b, tot, margin, _pct, proj in rollup(reg, confs)}
rows = []
for rid, _title, _prio in REQS:
entries = [(s, r) for s in dict.fromkeys(allocation_for(alloc, rid) + ["SYS"])
for r in (confs.get(s) or {}).get("requirements") or [] if isinstance(r, dict) and r.get("id") == rid]
own = [(s, r) for s, r in entries if s != "SYS" and r.get("status") in rank]
bname = alloc_budget(alloc, rid)
broll = budget_status(*rolled[bname]) if bname in rolled else None
main = next((r for s, r in entries if s == "SYS"), None) or broll or (
min(own, key=lambda e: rank.index(e[1]["status"]))[1] if own else {"status": "not_addressed"})
evid, missing = [], []
for s, r in entries:
for ref in (x.strip() for x in re.split(r"[;|]", str(r.get("evidence") or "")) if x.strip()):
found = evidence_path(ref, s)
(evid if found else missing).append(found or ref)
if main is broll:
evid += [f"02-concept/subsystems/{s}/conformance.yaml" for s in (rolled[bname][0].get("allocations") or {})
if os.path.isfile(os.path.join(subdir, s, "conformance.yaml"))]
if main.get("status") != "not_addressed" and not evid:
evid = [f"02-concept/subsystems/{s}/concept.md" for s, _ in entries
if s != "SYS" and os.path.exists(os.path.join(DELIV, "02-concept", "subsystems", s, "concept.md"))]
notes = [broll["note"]] if broll else []
notes += [f"{s}: {r.get('status')}" + (f" ({r['note']})" if r.get("note") else "") for s, r in entries]
notes += [f"evidence not found: {', '.join(missing)}"] if missing else []
notes += ["no subsystem reported this id"] if not entries else []
rows.append([rid, main.get("status"), main.get("value", ""), main.get("unit", ""),
";".join(dict.fromkeys(evid)), "; ".join(notes)])
if main.get("status") == "not_addressed":
rep.warn(f"{rid}: not addressed by any conformance entry")
os.makedirs(os.path.dirname(out_csv) or ".", exist_ok=True)
with open(out_csv, "w", newline="", encoding="utf-8") as fh:
csv.writer(fh).writerows([["req_id", "status", "value", "unit", "evidence", "note"]] + rows)
rep.note(f"{out_csv}: {len(rows)} rows")
def main():
ap = argparse.ArgumentParser(description="Contract registry check and tools (modes: see the file header).")
ap.add_argument("--registry", default=REG_DIR)
ap.add_argument("--subsystems-dir", default=SUB_DIR)
ap.add_argument("--subsystem", help="count only what concerns this subsystem (owners' self-check)")
ap.add_argument("--release", action="store_true", help="open amendments, missing conformance, unfrozen edits = errors")
ap.add_argument("--json", action="store_true", help="print a JSON summary as the last line")
g = ap.add_mutually_exclusive_group()
g.add_argument("--ids", action="store_true")
g.add_argument("--candidates", action="store_true")
g.add_argument("--freeze", metavar="NOTE")
g.add_argument("--brief", metavar="S")
g.add_argument("--route", metavar="AMD")
g.add_argument("--vote", nargs=4, metavar=("AMD", "S", "VERDICT", "REASON"))
g.add_argument("--merge", metavar="AMD")
g.add_argument("--reject", nargs=2, metavar=("AMD", "REASON"))
g.add_argument("--alternatives", action="store_true")
g.add_argument("--compliance", metavar="OUT_CSV")
args = ap.parse_args()
for k in ("registry", "subsystems_dir", "compliance"): # typed paths are relative to the caller's cwd
if getattr(args, k) and getattr(args, k) != ap.get_default(k):
setattr(args, k, os.path.abspath(getattr(args, k)))
anchor_run_dir()
REQS[:] = requirement_info()
rep, regdir, subdir = Report(focus=args.subsystem), args.registry, args.subsystems_dir
if pint is None:
rep.warn("pint is not importable: units are compared as text (run with ddpy)")
if args.ids:
print(f"# {len(REQS)} requirement ids of INPUT/requirements.md: allocate each to subsystem ids and/or SYS")
print("allocation:\n" + "\n".join(f" {r}: [] # {t}" + (f" (priority {fmt(p)})" if p else "")
for r, t, p in REQS))
return 0
if args.candidates:
mode_candidates(regdir, rep)
elif args.freeze:
mode_freeze(regdir, subdir, args.freeze, rep)
elif args.brief:
mode_brief(regdir, args.brief, rep)
elif args.route:
mode_route(regdir, args.route, rep)
elif args.vote:
mode_vote(regdir, *args.vote, rep)
elif args.merge:
mode_merge(regdir, subdir, args.merge, rep)
elif args.reject:
mode_reject(regdir, *args.reject, rep)
elif args.alternatives:
mode_alternatives(regdir, subdir, rep)
elif args.compliance:
mode_compliance(regdir, subdir, args.compliance, rep)
else:
mode_check(regdir, subdir, rep, args.release)
for tag, msgs in (("NOTE ", rep.notes), ("WARN ", rep.warnings), ("ERROR", rep.errors)):
for msg in msgs:
print(f"{tag} {msg}")
print(f"contract_check: v{read_version(regdir)}{' subsystem=' + args.subsystem if args.subsystem else ''} "
f"errors={len(rep.errors)} warnings={len(rep.warnings)} -> {'PASS' if not rep.errors else 'FAIL'}")
if args.json:
print(json.dumps({"version": read_version(regdir), "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/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/integration_check.py (206 wierszy)
#!/usr/bin/env python3
"""integration_check.py - full-tier integration checks of a CONTRACT-method run against the contract registry.
Run: ddpy method/gates/integration_check.py [--subsystem S] [--json] (works in the run directory it lives in)
1 CAD vs envelopes: every part of deliverables/03-cad/<S>/subassembly.step (placed in the product frame) lies in
one of S's envelope boxes; a shell subsystem lies in the product envelope (step-info)
2 assembly: deliverables/03-cad/assembly.step is valid, inside the product envelope and free of interference,
except pairs listed in deliverables/03-cad/interference-waivers.txt as "partA x partB: reason" (step-info)
3 pin maps: for every contract param with pins and refs, the connector pins in S's KiCad schematic
(deliverables/06-electronics/<S>/<name>.kicad_pro + .kicad_sch) sit on the contract's net names (kicad-cli)
4 BOM vs budgets: for budgets with bom_quantity, sum(qty x unit_price_<q>) of
deliverables/02-concept/subsystems/<S>/bom.csv <= S's allocation and within 5 % of S's conformance actual
5 the contract release check (contract_check.py --release)
Exit code 0 = no ERROR.
"""
import argparse
import csv
import glob
import json
import os
import shutil
import subprocess
import sys
import xml.etree.ElementTree as ET
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import contract_check as cc # noqa: E402 (re-runs this script under ddpy when pint/yaml are missing)
CAD = os.path.join(cc.DELIV, "03-cad")
ELEC = os.path.join(cc.DELIV, "06-electronics")
TOL = 0.5 # mm
def tool(cmd, timeout=900, env=None):
if not shutil.which(cmd[0]):
return None, f"{cmd[0]} not found (see INPUT/TOOLCHAIN.md)"
try:
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=env), None
except subprocess.TimeoutExpired:
return None, f"{cmd[0]} timed out after {timeout} s"
def step_info(path, interference=False):
p, err = tool(["step-info", path] + (["--interference"] if interference else []))
if err:
return None, err
try:
info = json.loads(p.stdout)
except json.JSONDecodeError:
return None, f"step-info gave no JSON for {path}: {p.stderr.strip()[-200:]}"
return (info, None) if info.get("ok") else (None, f"{path} is not a readable STEP file")
def excess(bb, box):
return max(max(box[i] - bb[i], bb[i + 3] - box[i + 3], 0) for i in range(3))
def check_cad(reg, subs, rep, only):
env = {e.get("subsystem"): e for e in reg.get("envelopes") or [] if isinstance(e, dict)}
pe = reg.get("product_envelope_mm")
for s in (x for x in subs if not only or x == only):
path = os.path.join(CAD, s, "subassembly.step")
if not os.path.isfile(path):
rep.error(f"{s}: {path} missing (the sub-assembly placed in the product frame)", s)
continue
info, err = step_info(path)
if err:
rep.error(f"{s}: {err}", s)
continue
if not info.get("all_valid"):
rep.error(f"{s}: {path} contains invalid shapes", s)
e = env.get(s)
boxes = [[cc.num(x) for x in pe]] if e and e.get("shell") and pe else \
[[cc.num(x) for x in b] for b in cc.boxes_of(e) if cc.box_ok(b)]
if not boxes:
rep.warn(f"{s}: no envelope in the contract; CAD position not checked", s)
continue
for part in info.get("part_list") or []:
bb = part.get("bbox_mm")
if bb and not any(cc.box_inside(bb, b, TOL) for b in boxes):
rep.error(f"{s}: part '{part.get('name')}' bbox {[round(x, 1) for x in bb]} leaves its envelope by "
f"{min(excess(bb, b) for b in boxes):.1f} mm", s)
rep.note(f"{s}: sub-assembly with {info.get('parts')} parts, bbox {info.get('bbox_mm')} mm")
def check_assembly(reg, rep):
path = os.path.join(CAD, "assembly.step")
if not os.path.isfile(path):
return rep.warn(f"{path} not built yet")
info, err = step_info(path, interference=True)
if err:
return rep.error(err)
if not info.get("all_valid"):
rep.error(f"{path} contains invalid shapes")
wfile = os.path.join(CAD, "interference-waivers.txt")
waivers = [line.split(":")[0].lower().split(" x ") for line in open(wfile) if " x " in line] \
if os.path.isfile(wfile) else []
for c in info.get("interference") or []:
pair = f"{c.get('a')} x {c.get('b')}"
waived = any(all(n.strip() and n.strip() in pair.lower() for n in w) for w in waivers)
(rep.note if waived else rep.error)(f"assembly interference {pair}: {c.get('overlap_mm3', c.get('error'))} mm3"
+ (" (waived)" if waived else ""))
pe = reg.get("product_envelope_mm")
if pe and info.get("bbox_mm") and not cc.box_inside(info["bbox_mm"], [cc.num(x) for x in pe], TOL):
rep.error(f"assembly bbox {info['bbox_mm']} exceeds product_envelope_mm {pe}")
rep.note(f"assembly: {info.get('parts')} parts, size {info.get('size_mm')} mm")
def netlist(s, rep):
"""{(ref, pin): net name} of subsystem s's schematic, exported with kicad-cli; None if s has no project."""
pros = sorted(glob.glob(os.path.join(ELEC, s, "**", "*.kicad_pro"), recursive=True))
sch = os.path.splitext(pros[0])[0] + ".kicad_sch" if pros else None
if not sch or not os.path.isfile(sch):
return None
out = os.path.join("work", "integration", f"{s}-netlist.xml")
os.makedirs(os.path.dirname(out), exist_ok=True)
env = dict(os.environ, KICAD_CONFIG_HOME=os.path.abspath(os.path.join("work", "kicad-config")))
p, err = tool(["kicad-cli", "sch", "export", "netlist", "--format", "kicadxml", "-o", out, sch], 300, env)
if err or p.returncode != 0 or not os.path.isfile(out):
rep.error(f"{s}: netlist export of {sch} failed: {err or p.stderr.strip()[-200:]}", s)
return {}
return {(node.get("ref"), node.get("pin")): net.get("name", "")
for net in ET.parse(out).getroot().iter("net") for node in net.iter("node")}
def check_pins(reg, subs, rep, only):
cache = {}
for itf in reg.get("interfaces") or []:
for p in (p for p in itf.get("params") or [] if isinstance(p, dict) and isinstance(p.get("pins"), dict)):
name, refs = f"{itf.get('id')}.{p.get('name')}", p.get("refs") or {}
per_side = all(isinstance(v, dict) for v in p["pins"].values())
for s in (x for x in cc.parties(itf) if x in subs and (not only or x == only)):
if s not in refs:
rep.warn(f"{s}: {name} has no refs entry for {s}; pin map NOT RUN", s)
continue
if s not in cache:
cache[s] = netlist(s, rep)
if cache[s] is None:
rep.warn(f"{s}: no KiCad project in {os.path.join(ELEC, s)}; pin map {name} NOT RUN", s)
continue
for pin, net in (p["pins"].get(s, {}) if per_side else p["pins"]).items():
got = cache[s].get((str(refs[s]), str(pin)))
if got is None:
rep.error(f"{s}: {name}: {refs[s]} pin {pin} is not in the schematic netlist", s)
elif got.split("/")[-1].strip().lower() != str(net).strip().lower():
rep.error(f"{s}: {name}: {refs[s]} pin {pin} is on net '{got}', the contract says '{net}'", s)
def check_bom(reg, subs, rep, only):
confs = cc.load_all_conf(reg, cc.SUB_DIR, cc.Report())
for b in (b for b in reg.get("budgets") or [] if isinstance(b, dict) and b.get("bom_quantity")):
col = f"unit_price_{int(cc.num(b['bom_quantity']) or 0)}"
for s, alloc in (b.get("allocations") or {}).items():
if s not in subs or (only and s != only):
continue
path = os.path.join(cc.SUB_DIR, s, "bom.csv")
if not os.path.isfile(path):
rep.error(f"{s}: {path} missing (budget {b['name']})", s)
continue
rows = list(csv.DictReader(open(path, encoding="utf-8-sig")))
priced = [(cc.num(r.get("qty")), cc.num(r.get(col))) for r in rows]
total = sum(q * u for q, u in priced if None not in (q, u))
if any(None in x for x in priced):
rep.warn(f"{s}: {sum(None in x for x in priced)} bom.csv line(s) without numeric qty or {col}", s)
curs = {(r.get("currency") or "").strip() for r in rows} - {""}
if curs != {str(b.get("unit"))}:
rep.warn(f"{s}: bom.csv currency {sorted(curs)} is not only the budget unit {b.get('unit')}; not "
f"compared - convert every price to {b.get('unit')} (rate, its source and date in notes)", s)
continue
if total > (cc.num(alloc) or 0) + 1e-9:
rep.error(f"{s}: BOM {col} sum {total:.2f} {b['unit']} exceeds the {b['name']} allocation {alloc}", s)
act = cc.principle_actual(confs.get(s), None, b)
if act is not None and abs(act - total) > 0.05 * max(total, 1e-9):
rep.warn(f"{s}: conformance {b['name']} {act:.2f} differs from the BOM sum {total:.2f} by > 5 %", s)
rep.note(f"{s}: BOM {col} sum {total:.2f} {b['unit']} vs allocation {alloc}")
def main():
ap = argparse.ArgumentParser(description="Full-tier integration checks (see the file header).")
ap.add_argument("--subsystem", help="check only this subsystem (owners' self-check)")
ap.add_argument("--json", action="store_true", help="print a JSON summary as the last line")
args = ap.parse_args()
cc.anchor_run_dir()
cc.REQS[:] = cc.requirement_info()
rep = cc.Report(focus=args.subsystem)
cc.mode_check(cc.REG_DIR, cc.SUB_DIR, rep, release=True)
reg = cc.load_registry(cc.REG_DIR, cc.Report())
subs = cc.sub_ids(reg)
check_cad(reg, subs, rep, args.subsystem)
if not args.subsystem:
check_assembly(reg, rep)
check_pins(reg, subs, rep, args.subsystem)
check_bom(reg, subs, rep, args.subsystem)
for tag, msgs in (("NOTE ", rep.notes), ("WARN ", rep.warnings), ("ERROR", rep.errors)):
for msg in msgs:
print(f"{tag} {msg}")
print(f"integration_check:{' subsystem=' + args.subsystem if args.subsystem else ''} errors={len(rep.errors)} "
f"warnings={len(rep.warnings)} -> {'PASS' if not rep.errors else 'FAIL'}")
if args.json:
print(json.dumps({"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/roles/subsystem-owner.md (146 wierszy)
# Role: subsystem owner
You own ONE subsystem of a hardware product end to end - every part, circuit, line of code and cost inside its
boundary - and you design it against a frozen, machine-checked contract written by the architect (the top
session). Other owners design the other subsystems at the same time against the same contract. You never talk to
them: the contract is the interface. Nobody answers questions: decide by `method/POLICY.md`, record the decision.
Paths are relative to the run directory. `subsystems/<S>/` below means `deliverables/02-concept/subsystems/<S>/`:
your folder, holding `concept.md`, `calc/`, `bom.csv`, `conformance.yaml`, `STATE.md` (your plan and progress,
the file a later pass resumes from) and, in the detail passes, `hazards.md` and `tests.md`.
## Read first
1. `method/HYGIENE.md` (binding) and `method/POLICY.md` sections 1, 2, 4.
2. Your contract view: `ddpy method/gates/contract_check.py --brief <S>` - scope, envelope boxes, your
obligations, the values guaranteed to you, budget allocations, allocated requirement ids, a conformance
skeleton with the exact names.
3. `INPUT/requirements.md` (your allocated ids in full) and `INPUT/TOOLCHAIN.md` (commands).
4. The handbook `.claude/skills/dd-hw-handbook/SKILL.md`: sections 5, 6, 11, 12 for the CONCEPT pass; sections
6-14 for the detail passes.
5. When needed: `deliverables/02-concept/contracts/REGISTRY.md` and the registry YAML files (read only).
Run the gate scripts; do not read their source unless a traceback forces you to.
## Write rights
- Yours: `deliverables/02-concept/subsystems/<S>/` and `work/<S>/` (scratch and downloads; not part of the deliverables).
- Detail passes also: `deliverables/03-cad/<S>/`, `deliverables/04-drawings/<S>/`,
`deliverables/05-calculations/<S>-*.py` with their results, `deliverables/06-electronics/<S>/`, and
`deliverables/07-firmware/` only when your contract view says you own the firmware.
- You may create new files `deliverables/02-concept/contracts/amendments/AMD-<S>-<nn>.yaml` and revise your own
while their `status` is still `proposed`.
- Nothing else: never the contract files, another subsystem, `01-requirements/`, `REPORT.md` or
`manifest.json`. Never create files named `REPORT*`, `SUMMARY*`, `FINDINGS*` or `ANALYSIS*`.
## Contract rules
- Design to the contract version named in your task. Values guaranteed to you: design for their whole range.
Your obligations: meet them and report the actual values.
- Stay inside your envelope boxes and every budget allocation. An actual above its allocation is a breach.
- Cannot meet a value after trying inside your boundary, or the contract lacks something you need (a signal,
a heat path, a parameter)? Propose an amendment - never deviate silently, never edit the contract. Copy
`method/templates/amendment.yaml` to `deliverables/02-concept/contracts/amendments/AMD-<S>-<nn>.yaml` (nn = 01,
02 ...): each change is `op: set` (a value; old -> new with numbers), `op: add` (`where` names the list or
mapping that receives the new param, interface, consumer or allocation entry) or `op: remove`; then the
rationale and the impact, and message the architect. If it answers with route errors, fix your file (still
`proposed`) and message again. Keep working on everything the amendment does not touch, against the current
version; note the dependency in STATE.md.
- Vote requests: answer within minutes, by POLICY section 2.
- `CONTRACT v<N> in force`: run your self-check; if it says STALE, update the changed values and
`contract_version`.
## Messages
- To the architect: SendMessage with `to: "main"` (load it once: ToolSearch `select:SendMessage`). One line
that starts with the subject: `AMD-<id> proposed: <summary>`, `AMD-<id> ACCEPT: <reason>` or
`AMD-<id> REJECT: <the number that breaks>; counter-proposal: <...>`.
- The architect's messages (vote request, CONTRACT, TIME, FIX, SWITCH) arrive by themselves between your tool
calls: act on them at once. TIME means: complete conformance.yaml with what you have (uncertain values
labelled ASSUMPTION), self-check, end within 3 minutes.
- No SendMessage available: write votes to `deliverables/02-concept/subsystems/<S>/votes.md` and repeat
proposals and votes in your final report.
## Conventions
- Every number has a home and an origin label (HYGIENE). Calculations: one ddpy script per question, in
`subsystems/<S>/calc/` (CONCEPT) or `deliverables/05-calculations/<S>-<topic>.py` (detail); inputs at the
top with origin labels; units with pint; paths relative to the script (`Path(__file__).parent`), never to
the working directory; the script writes `<stem>.results.json` beside itself
(`{"<name>": {"value": x, "unit": "..."}}`). Run every script and keep its results.
- Parts: real purchasable parts read from a supplier page or the offline catalogue named in
`INPUT/TOOLCHAIN.md`, with the date the price was read; everything else as labelled ASSUMPTION ranges. Web
pages: load WebSearch and WebFetch once with ToolSearch `select:WebSearch,WebFetch`. Your parts go to
`subsystems/<S>/bom.csv` with exactly the RUN CONTRACT bom.csv header, every price in the currency of your
cost budget (`--brief` shows it): convert other currencies and put the rate, its source and date in `notes`.
- Every requirement id allocated to you gets `met`, `partly` or `not_met` with the predicted value
(`not_addressed` fails the self-check; an id you cannot influence: `partly` or `not_met` with the reason in
`note`, named in your OWNER REPORT). At the concept tier every status is a prediction: say "predicted,
CALCULATION" (or the label that applies) in `note`.
- Your task's end time is binding: finish by it even if no TIME message arrives.
## CONCEPT pass (time box in your task, about 18 minutes)
1. `date`; write `subsystems/<S>/STATE.md` (start, deadline, plan).
2. Two principles A and B that differ in working principle: the architect's hint, or a better one with a reason.
3. For both: first-order numbers for every budget allocated to you and for the key metric of every priority 4-5
requirement allocated to you; hand estimate first, then a script.
4. The 1-3 real parts that decide feasibility (manufacturer, part number, supplier URL, price at 1/100/1000 and
the date read) into `bom.csv`, the rest as ASSUMPTION lines.
5. Recommend one principle (POLICY section 2). Fill `principles[]` with the budget actuals of BOTH principles:
the architect combines them into system alternatives.
6. `concept.md`: 1 scope and functions; 2 principles A and B (how each works, a text sketch, a numbers table,
critical parts, risks); 3 the recommendation and its deciding numbers; 4 interfaces provided and consumed,
with values; 5 budget actuals vs allocations; 6 requirement contributions (id, predicted value, status,
evidence); 7 hazards of the subsystem and the protections that work without firmware; 8 risks and open
issues; 9 what detailed design must do first.
7. `conformance.yaml` from the `--brief` skeleton (format: `method/templates/conformance.yaml`): every
obligation, every budget actual, every allocated requirement id.
8. Self-check `ddpy method/gates/contract_check.py --subsystem <S>` until it reports no ERROR for you (or the
only one left waits for your amendment).
9. Final report. No CAD, schematics or firmware in this pass.
## DETAIL-1 pass (full tier): geometry, parts, schematic
- Resume from `subsystems/<S>/STATE.md` and `concept.md`; run `--brief <S>` again (the detail contract adds
positions, connectors and pin maps).
- CAD in `deliverables/03-cad/<S>/`: one parametric build123d or CadQuery script per designed part (contract
values as named constants with their interface id), a STEP per part, and `subassembly.py` ->
`subassembly.step` placed in the PRODUCT FRAME of the contract, part names starting with `<S>`. Purchased
parts as simplified bodies with their datasheet envelope. Check: `step-info subassembly.step --interference`.
- Mass from CAD (volume x density) into the conformance budgets.
- `subsystems/<S>/bom.csv` complete: real parts, prices at 1/100/1000 with the read date.
- Electronics, if your subsystem has any: KiCad 9 project `deliverables/06-electronics/<S>/<S>.kicad_pro` +
`.kicad_sch` (recipe in `INPUT/TOOLCHAIN.md`); the nets at contract connectors named exactly as the contract
`pins`, connector references as in `refs`; ERC report.
- Self-checks: `contract_check.py --subsystem <S>` and `ddpy method/gates/integration_check.py --subsystem <S>`.
Update conformance.yaml and STATE.md.
## DETAIL-2 pass (full tier): completion
- PCB layout, DRC report and fabrication outputs (Gerbers, drill, positions, board BOM) in `06-electronics/<S>/`.
- Firmware (only the firmware owner): a PlatformIO project in `deliverables/07-firmware/` that builds offline
with the platform versions `INPUT/TOOLCHAIN.md` pins; a state machine with fault states and a safe default,
a watchdog, native host tests of the logic and interlocks; the pin map checked against the schematic.
- Calculations `deliverables/05-calculations/<S>-<topic>.py` for every allocated requirement verified by
calculation; each runs with ddpy from that folder in under 5 minutes and writes its results file.
- Drawings `deliverables/04-drawings/<S>/`: one per manufactured part (views, dimensions, tolerances, material,
finish, part number as in bom.csv), PDF or SVG; open every file you write.
- `subsystems/<S>/hazards.md` (hazard -> measure -> protection independent of firmware -> residual risk) and
`subsystems/<S>/tests.md` (one test per allocated requirement and per contract obligation: method,
equipment, pass criterion, id).
- Final self-checks; update conformance.yaml and STATE.md.
## Final report (your last message, at most 15 lines)
```
OWNER REPORT <S> pass=<CONCEPT|DETAIL-1|DETAIL-2|VOTE|FIX|SWITCH> contract=v<N>
status: done | partial (why) | blocked (why)
self-check: PASS | FAIL (<n> errors: ...)
principle: <id> <name> - <deciding numbers>
budgets: <name actual/allocation unit>; ...
requirements: <n> met, <n> partly, <n> not_met
amendments: <ids and status> | none
top risks: <1-3>
files: <main paths>
```
files/method/templates/allocation.yaml (15 wierszy)
# deliverables/02-concept/contracts/allocation.yaml - architect only. Every requirement id of
# INPUT/requirements.md goes to at least one subsystem, or to SYS (system level: integration, compliance route,
# documentation, packaging - the architect). Start from: ddpy method/gates/contract_check.py --ids
# short form: ID: [S1-core, S2-control]
# long form: ID: {to: [...], budget: <budget name>, interfaces: [IF-..], note: "..."}
# patterns: "ABC-SAF-*": [SYS, S2-control] (an exact id overrides a pattern; use exact ids for priority >= 4)
# Derived requirements may be allocated too: DR-<AREA>-<nn>. PLACEHOLDER ids below - replace them.
allocation:
REQ-PRF-01: [S1-core]
REQ-PRF-02: {to: [S1-core, S2-control], interfaces: [IF-01], note: "S2 supplies the power S1 converts"}
REQ-WGT-01: {to: [S1-core, S2-control, S3-housing], budget: mass}
REQ-CST-01: {to: [S1-core, S2-control, S3-housing, SYS], budget: unit_cost_q1000}
"REQ-CRT-*": [SYS]
files/method/templates/alternatives.yaml (29 wierszy)
# deliverables/02-concept/contracts/alternatives.yaml - architect. System alternatives = the chosen partition x
# the owners' principle choices: at least 3 that differ in the working principle of at least one critical
# subsystem. Budget closure is computed from each owner's per-principle actuals (conformance.yaml principles[]);
# a budget that does not close knocks the alternative out. Run: ddpy method/gates/contract_check.py --alternatives
# Criteria: one per requirement group; weight = the highest priority among its ids (method/POLICY.md section 2).
# Scores 0-10, each justified with numbers in concept.md. PLACEHOLDER values below.
criteria:
- {name: performance, weight: 5, reqs: [REQ-PRF-01, REQ-PRF-02]}
- {name: safety_compliance, weight: 5, reqs: [REQ-SAF-01, REQ-CRT-01]}
- {name: unit_cost, weight: 4, reqs: [REQ-CST-01]}
- {name: size_mass, weight: 3, reqs: [REQ-SIZ-01, REQ-WGT-01]}
- {name: risk, weight: 3, reqs: []}
alternatives:
- id: ALT-1
name: "<short name>"
choice: {S1-core: A, S2-control: A} # subsystems not listed use their recommended principle
scores: {performance: 8, safety_compliance: 7, unit_cost: 6, size_mass: 7, risk: 6}
- id: ALT-2
name: "<short name>"
choice: {S1-core: B}
scores: {performance: 6, safety_compliance: 8, unit_cost: 8, size_mass: 6, risk: 8}
- id: ALT-3
name: "<short name>"
choice: {S1-core: B, S2-control: B}
knocked_out: "REQ-PRF-02 threshold missed: predicted 72 % < 80 % (calc/...)" # optional manual knock-out + reason
scores: {performance: 4, safety_compliance: 8, unit_cost: 9, size_mass: 8, risk: 7}
files/method/templates/amendment.yaml (33 wierszy)
# deliverables/02-concept/contracts/amendments/AMD-<S>-<nn>.yaml - a request to change the frozen contract.
# Proposed by the owner of <S> (nn = 01, 02 ... per owner, so ids never collide) or by SYS (the architect).
# The proposer writes it with status proposed and tells the architect. While the status is still proposed (for
# example after --route found errors) the proposer may revise it; once routed, only the architect's tool edits it
# (contract_check.py --route / --vote / --merge / --reject). PLACEHOLDER values below.
id: AMD-S1-core-01
proposed_by: S1-core
against_version: 1
summary: "Raise IF-01 current_draw max from 2.0 A to 2.4 A; S1 input power allocation 40 -> 46 W"
rationale: "<numbers: why the current contract cannot be met, what you tried inside your boundary>"
impact: "<who must change what; which budget or reserve absorbs it>"
changes: # where = <section>/<id or name>/.../<field>; list items are found by id, name or subsystem (or value)
- where: interfaces/IF-01/params/current_draw
op: set # set (default): merge 'new' into a mapping (null deletes a key) or replace a value
old: {max: 2.0}
new: {max: 2.4}
- where: budgets/input_power/allocations/S1-core
old: 40
new: 46
- where: interfaces/IF-01/params # add: 'where' names the list or mapping that receives 'new' (an item, a list
op: add # of items, or new keys); a new interface: where: interfaces
new: {name: inrush_current, rule: within, party: S1-core, max: 5.0, unit: A, origin: "CALCULATION calc/inrush.py"}
- where: interfaces/IF-02/params/old_param # remove: 'where' names the list item or mapping key to delete
op: remove
# Large revisions (architect, e.g. the detail contract of the full tier): instead of changes, put the complete
# proposed files (any of interfaces.yaml, budgets.yaml, allocation.yaml) in amendments/<this id>/ and write:
# proposed_registry: AMD-SYS-<nn> # = this amendment's id (the next free AMD-SYS number)
# basis: system_budget_violation # SYS only: lets the architect merge without votes while the check reports a
# SYSTEM BUDGET VIOLATION in the budgets it changes - only for the reserve or a derived limit (POLICY section 2
# steps 1 and 4); --merge refuses it when it lowers an owner's allocation or changes anything else: route those
status: proposed # proposed -> routed -> accepted | rejected -> merged (set by the tool)
files/method/templates/budgets.yaml (42 wierszy)
# deliverables/02-concept/contracts/budgets.yaml - architect only. PLACEHOLDER values: replace all of them.
# Budgets are upper limits: the combined value of the subsystems must stay <= limit.
# combine: sum (mass, cost, power, heat into the enclosure ...) | max (each allocation is a cap, e.g. a surface
# temperature) | db (energetic sum of sound levels: 10*log10(sum 10^(L/10))).
# The reserve stays unallocated: combine(allocations) <= limit - reserve (reserve_pct of the limit, or
# reserve_abs in the budget unit; defaults from method/POLICY.md section 3).
# Owners report actuals in conformance.yaml; actual <= allocation. SYS = the architect's system-level share.
budgets:
- name: mass
unit: kg
limit: 4.0 # the requirement threshold (or a derived limit with its origin)
goal: 3.0 # the requirement target (optional; margins are reported against it too)
origin: "<REQ-ID> threshold 4.0 kg, target 3.0 kg"
reserve_pct: 10
combine: sum
allocations: {S1-core: 1.5, S2-control: 0.6, S3-housing: 1.4}
estimates: {S1-core: 1.3, S2-control: 0.5, S3-housing: 1.2} # first estimates (candidates, sanity check)
- name: unit_cost_q1000
unit: EUR
limit: 60
origin: "<REQ-ID> threshold"
reserve_pct: 15
combine: sum
allocations: {S1-core: 20, S2-control: 14, S3-housing: 9, SYS: 8} # SYS: assembly, test, packaging
bom_quantity: 1000 # full tier: checked against subsystems/<S>/bom.csv (qty x unit_price_1000);
# bom.csv prices in this unit's currency (conversion rate as SOURCE in notes)
- name: input_power
unit: W
limit: 60
origin: "<REQ-ID> or DR-PWR-01 (derived: supply rating)"
reserve_pct: 10
combine: sum
allocations: {S1-core: 40, S2-control: 8, S3-housing: 2}
- name: touch_temperature
unit: degC
limit: 60
origin: "<REQ-ID> / applicable safety standard clause (SOURCE)"
reserve_abs: 5
combine: max
allocations: {S1-core: 55, S2-control: 50, S3-housing: 50}
files/method/templates/candidate.yaml (34 wierszy)
# deliverables/02-concept/contracts/candidates/P<n>.yaml - one candidate partition, sketch depth (architect).
# Write 2-3 candidates that cut the product DIFFERENTLY: where the module boundaries sit, which functions share a
# module, what crosses an interface (e.g. integrated vs separate power module, UI on the housing vs on the
# controller). Each subsystem is a physical module with its own envelope that owns everything inside its
# boundary across disciplines - never a split by discipline ("all electronics", "all mechanics").
# Same sections as the registry (formats: interfaces.yaml, budgets.yaml, allocation.yaml in this folder) at
# sketch depth: interfaces need id, kind, title, provider, consumers (params optional); budgets need limit and
# estimates per subsystem; allocation may use patterns. Compare: ddpy method/gates/contract_check.py --candidates
# Keep it a sketch (about 3 minutes per candidate). The winner becomes the registry files, written by hand in
# step C at contract depth. PLACEHOLDER values below.
subsystems:
- {id: S1-core, name: "Core function module", scope: "..."}
- {id: S2-control, name: "Power and control", scope: "...", owns_firmware: true}
- {id: S3-housing, name: "Enclosure and user interface", scope: "..."}
interfaces:
- {id: IF-01, kind: electrical, title: "DC supply S2 -> S1", provider: S2-control, consumers: [S1-core]}
- {id: IF-02, kind: data, title: "Sensor and actuator signals S1 <-> S2", provider: S1-core, consumers: [S2-control]}
- {id: IF-03, kind: mechanical, title: "S1 mounted in S3", provider: S3-housing, consumers: [S1-core]}
- {id: IF-04, kind: user, title: "Controls and display", provider: S3-housing, consumers: [S2-control]}
budgets:
- {name: mass, unit: kg, limit: 4.0, origin: "<REQ-ID>", reserve_pct: 10, combine: sum,
estimates: {S1-core: 1.3, S2-control: 0.5, S3-housing: 1.2}}
- {name: unit_cost_q1000, unit: EUR, limit: 60, origin: "<REQ-ID>", reserve_pct: 15, combine: sum,
estimates: {S1-core: 18, S2-control: 12, S3-housing: 8, SYS: 7}}
allocation:
REQ-PRF-01: [S1-core]
REQ-PRF-02: [S1-core, S2-control]
"REQ-SAF-*": [S2-control, S3-housing]
"REQ-CRT-*": [SYS]
files/method/templates/conformance.yaml (47 wierszy)
# deliverables/02-concept/subsystems/<S>/conformance.yaml - written by the owner of <S> only.
# Start from the skeleton printed by: ddpy method/gates/contract_check.py --brief <S> (exact names and units)
# Self-check: ddpy method/gates/contract_check.py --subsystem <S>
# Values: a number, [lo, hi] for a range, or the exact contract text/pins for an "equals" param. Every value has
# an origin (SOURCE with URL and read date / CALCULATION with the script / ASSUMPTION with the reason).
# Evidence paths: relative to your subsystem folder (calc/x.results.json) or to deliverables/. PLACEHOLDERS below.
subsystem: S1-core
contract_version: 1 # the contract version this file was checked against
principle: A # the principle this file describes (your recommendation, or the one the architect chose)
principles: # exactly the two you compared; they differ in working principle
- id: A
name: "<short name>"
summary: "<how it works, one line>"
verdict: recommended # recommended | rejected
reason: "<the numbers that decided>"
budgets: # this principle's actuals, same budget names as below
mass: {value: 1.3, unit: kg, origin: "CALCULATION calc/mass_A.py"}
unit_cost_q1000: {value: 17.5, unit: EUR, origin: "SOURCE bom.csv (prices read 2026-..)"}
- id: B
name: "<short name>"
summary: "<how it works, one line>"
verdict: rejected
reason: "<the numbers that decided>"
budgets:
mass: {value: 1.6, unit: kg, origin: "CALCULATION calc/mass_B.py"}
unit_cost_q1000: {value: 14.0, unit: EUR, origin: "ASSUMPTION: catalogue-class parts, +-30 %"}
obligations: # one entry per contract param you must satisfy (the --brief table)
- {interface: IF-01, param: current_draw, value: 1.6, unit: A, origin: "CALCULATION calc/power.py"}
- {interface: IF-01, param: rail_voltage_tolerated, value: [9.0, 14.0], unit: V,
origin: "SOURCE <regulator datasheet URL>, read 2026-.."}
- {interface: IF-01, param: connector, value: "<exactly the contract text>", origin: "contract v1"}
budgets: # actuals of the principle above, one per budget that allocates to you
mass: {value: 1.3, unit: kg, origin: "CALCULATION calc/mass_A.py"}
unit_cost_q1000: {value: 17.5, unit: EUR, origin: "SOURCE bom.csv"}
requirements: # one entry per requirement id allocated to you; at the concept tier a prediction;
# status met | partly | not_met (not_addressed fails the self-check)
- {id: REQ-PRF-01, status: met, value: 0.8, unit: s, evidence: "calc/response.results.json",
note: "predicted, CALCULATION"}
- {id: REQ-PRF-02, status: partly, value: 84, unit: "%", evidence: "concept.md",
note: "predicted: below target 90, above threshold 80; limited by the IF-01 power allocation"}
amendments: [] # ids of the amendments you proposed
files/method/templates/interfaces.yaml (100 wierszy)
# deliverables/02-concept/contracts/interfaces.yaml - written by the architect only (owners propose amendments).
# Every value below is a PLACEHOLDER showing the format - replace all of them with this product's values.
# Units: any pint unit ("mm", "kg", "W", "V", "A", "degC", "l/min", "EUR", "1" for a plain number); units that
# start with "dB" and counts ("pcs") are compared as text, never converted. Every number has an origin.
# Ids: subsystems S<n>-<slug>; external parties EXT:<name> (user, supply, mounting surface, environment ...);
# interfaces IF-<nn>. Check: ddpy method/gates/contract_check.py (the architect freezes with --freeze).
frame: "product frame: origin at the rear-left-bottom corner of the product envelope; X right, Y front, Z up; mm"
product_envelope_mm: [0, 0, 0, 240, 160, 120] # [x0, y0, z0, x1, y1, z1]; origin: the size requirement
subsystems:
- id: S1-core
name: "Core function module"
scope: "what is inside its boundary (functions, parts, its own board and code if any) and what is not"
principles: ["A: <working principle>", "B: <a different working principle>"] # hint for the owner
- id: S2-control
name: "Power and control"
scope: "..."
owns_firmware: true # exactly one subsystem owns 07-firmware/ (the one with the main controller)
- id: S3-housing
name: "Enclosure and user interface"
scope: "..."
envelopes: # space claims in the product frame; boxes of different subsystems must not overlap
- subsystem: S1-core
boxes: [[10, 10, 10, 150, 150, 100]] # several boxes allowed for L-shapes
origin: "ASSUMPTION: first layout sketch, see 02-concept/concept.md section 5"
- subsystem: S2-control
boxes: [[160, 10, 10, 230, 150, 60]]
origin: "ASSUMPTION: first layout sketch"
- subsystem: S3-housing
shell: true # the outer shell may enclose the other envelopes (checked against the product envelope)
boxes: [[0, 0, 0, 240, 160, 120]]
origin: "= product envelope"
interfaces:
- id: IF-01
kind: electrical # mechanical | electrical | thermal | fluid | data | optical | acoustic | user |
# software | magnetic | chemical | other
title: "DC supply S2 -> S1"
provider: S2-control
consumers: [S1-core]
params:
- name: rail_voltage
rule: within # within: the party's actual value or [lo, hi] lies inside [min, max]
min: 11.4 # default party = the provider
max: 12.6
unit: V
origin: "ASSUMPTION: regulated rail +-5 %"
- name: rail_voltage_tolerated
rule: covers # covers: the party's tolerated range contains [min, max]
min: 11.4 # default party = the consumers
max: 12.6
unit: V
origin: "= rail_voltage"
- name: current_draw
rule: within
party: S1-core # name the party whenever it is not the default
max: 2.0
unit: A
origin: "CALCULATION: 02-concept/calc/power_estimate.py"
- name: connector
rule: equals # equals: every party reports exactly this value (default party = all parties)
value: "<connector family, pitch, pin count, keying>"
origin: "ASSUMPTION"
- name: pinout
rule: equals
pins: {"1": "+12V", "2": "GND"} # pin -> net name; every board names these nets exactly so
refs: {S2-control: J1, S1-core: J1} # connector reference per side (full tier: netlist check)
origin: "ASSUMPTION"
- id: IF-02
kind: mechanical
title: "S1 mounted in S3"
provider: S3-housing
consumers: [S1-core]
params:
- name: mounting
rule: equals
value: "<n x M<d> at (x, y) mm in the product frame, boss height h mm>"
origin: "ASSUMPTION"
- name: supported_mass
rule: within
party: S1-core
max: 1.5
unit: kg
origin: "ASSUMPTION"
- id: IF-03
kind: user
title: "Operator presses the main control"
provider: EXT:user
consumers: [S3-housing]
params:
- name: actuation_force
rule: within
party: S3-housing
min: 2
max: 6
unit: N
origin: "SOURCE: <ergonomics reference URL, read date>"
files/method/templates/owner-tasks.md (50 wierszy)
# Owner task prompts and message texts (architect: fill every <...>)
Launch all owners of a pass in ONE message, one call each:
`Agent({description: "owner <S> <PASS>", subagent_type: "subsystem-owner", prompt: "<prompt below>"})`
Record each returned agent id in state/ARCH-STATE.md.
## CONCEPT prompt
Run directory: <absolute run dir>. You are the owner of subsystem <S> ("<name>"). Pass: CONCEPT. Contract
version: v<N>. Clock: now <HH:MM>; wrap up by <HH:MM>; end by <HH:MM> at the latest (check with `date`; these
times bind you even if no TIME message comes).
1. Read method/HYGIENE.md and method/roles/subsystem-owner.md and follow them.
2. Run `ddpy method/gates/contract_check.py --brief <S>`: your scope, envelope, obligations, budgets, ids.
3. Principles to compare (hint; replace one only with a better one and say why): A <...>; B <...>.
4. What decides this subsystem - look at it first: <1-3 items: dominant requirement ids, the tightest budget,
the critical part>.
Write only inside deliverables/02-concept/subsystems/<S>/ and work/<S>/ (plus new amendment files). End with
the OWNER REPORT block of your role file.
## DETAIL-1 and DETAIL-2 prompts (full tier, fresh agents)
Run directory: <absolute run dir>. You are the owner of subsystem <S> ("<name>"). Pass: DETAIL-<1|2>. Contract
version: v<N>. Clock: now <HH:MM>; wrap up by <HH:MM>; end by <HH:MM> (binding even without a TIME message).
1. Read method/HYGIENE.md and method/roles/subsystem-owner.md and follow them.
2. Run `ddpy method/gates/contract_check.py --brief <S>`; resume from
deliverables/02-concept/subsystems/<S>/STATE.md and concept.md.
3. Do the DETAIL-<1|2> work of your role file. Fix first: <errors of the last integration check, with numbers>.
End with the OWNER REPORT block.
## VOTE prompt (only when SendMessage is unavailable)
Run directory: <absolute run dir>. You are the owner of subsystem <S>. Pass: VOTE. Read
method/roles/subsystem-owner.md (sections "Contract rules" and "Messages"), run
`ddpy method/gates/contract_check.py --brief <S>` and read
deliverables/02-concept/contracts/amendments/<AMD-id>.yaml. Decide by method/POLICY.md section 2, append
`<AMD-id> ACCEPT|REJECT: <reason>` to deliverables/02-concept/subsystems/<S>/votes.md and end with that line.
Time box 5 minutes.
## Message texts (SendMessage to the owner's agent id)
- Vote request: printed by `--route`.
- `CONTRACT v<N> in force: <AMD-id> merged - <summary>. Run ddpy method/gates/contract_check.py --subsystem <S>;
if it says STALE, update conformance.yaml (values and contract_version: <N>), then report in one line to main.`
- `TIME: wrap up now. Complete conformance.yaml with what you have (uncertain values labelled ASSUMPTION), run
your self-check and end with your OWNER REPORT within 3 minutes.`
- `FIX: <check line>. <what is wrong, with the number>. Fix it in your files or propose an amendment, self-check,
then end with an updated OWNER REPORT.`
- `SWITCH: the system decision uses principle <X> for <S> because <reason>. Make <X> the principle of
conformance.yaml (obligations, budgets, requirements), self-check, then end with an updated OWNER REPORT.`
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.