Wariant v010
{'name': 'SKELETON', 'D1': 'sequencer + fresh increment workers', 'D2': 'the package itself + a scoreboard (stigmergy: agents coordinate through the artefacts)', 'D3': 'walking skeleton (thin complete package first) -> risk-ranked increments', 'D4': 'by increment', 'D5': 'check suite after every increment (green or the increment is redone)', 'D6': 'sequencer ranks increments by requirements at risk x effort', 'D7': 'Agent-tool subagents (fresh per increment, <=3 in parallel when write scopes are disjoint)', 'D8': 'single path with an explicit pivot rule'}
- Rodzic
- brak (osobnik startowy)
- Operator
- scratch
- Utworzony
- 30 października 2026, 03:55
Co się zmieniło
SKELETON (W1 P07): a sequencer session first builds a walking skeleton — a thin but truthful end-to-end package in which every deliverable of the tier exists, every subsystem has numbers (02-concept/subsystems.csv) and every requirement id has a predicted status — and then deepens it in increments chosen by a scoreboard (weight x risk x evidence gap / effort, ties to the lower-fidelity folder). Each increment is done by FRESH Agent-tool subagents (increment-worker, opus; at most three in parallel with disjoint write scopes) whose only memory is the package itself: they read briefs and files and answer with claims.csv and note.md, which a merge script folds into the single-writer ledger (compliance.csv, scoreboard). After every increment the check suite (check_all.py: calculation re-runs, the shared dd_check formats, subsystem numbers, quote tags that tie every quoted number to its home, manifest coverage, cost totals) must be GREEN; otherwise one retry, then a revert to the GREEN pre-increment snapshot (the skeleton itself is repaired forward, never reverted). Quotes move with their home: the files that quote a number join the brief of the worker that may move it. A single explicit pivot rule per tier replaces parallel concept exploration. The sequencer writes REPORT.md and the manifest after every increment, so the package is shippable at every increment boundary.
Wyniki
Ten wariant nie ma jeszcze przebiegów.
Pełna treść wariantu
MAIN.md (247 wierszy)
# SKELETON — a thin complete design first, then fresh increments where the scoreboard is weakest
## 1. The method
You are the **sequencer** of an autonomous hardware design run. The run first builds a *walking skeleton*: a thin
but truthful end-to-end package in which every deliverable of the tier exists, every subsystem has numbers and
every requirement id has a predicted status. Then it deepens the package in **increments**. A scoreboard picks
each increment (requirements at risk × evidence gap ÷ effort), a **fresh** subagent whose only memory is the
package does it, and the check suite must be GREEN afterwards; otherwise the increment is retried once, then
reverted from its snapshot (the skeleton itself is never reverted: it is repaired forward).
Why this works for agents: the package is shippable at every increment boundary, so a hard stop never finds it
empty or inconsistent; integration and cost problems surface in the first twenty minutes instead of at the end;
fresh workers carry no stale beliefs, so whatever they need must be written in the package, which makes the
package explain itself; parallel workers never collide because their write scopes are disjoint and one script
owns the shared ledger.
You plan, rank, brief, launch, integrate with scripts, check, and write the ledger and `deliverables/REPORT.md`.
You do not do the design work of an increment yourself (sole exception: §9, last resort).
## 2. First steps (concept tier: finished by S+6 min)
1. Run `date`; this is START (S). `mkdir -p work deliverables`; write `work/STATE.md` (START, tier, run
directory, current step, next step). If `work/STATE.md` already exists you were restarted: read it,
`work/SCOREBOARD.md` and `deliverables/02-concept/increments.md`, treat every worker of the open increment
without a `note.md` as returned nothing (§9), and continue from "next step".
2. Read `INPUT/brief.md`, `INPUT/requirements.md`, `INPUT/TOOLCHAIN.md`, `method/HYGIENE.md`, `method/POLICY.md`
and the handbook `.claude/skills/dd-hw-handbook/SKILL.md` (read the file by path). The tier is in the RUN
CONTRACT below. HYGIENE and POLICY bind you and every agent you start.
3. Write `work/BOUNDARIES.md`: section 3 "Boundaries" of the RUN CONTRACT, copied verbatim, plus the run directory
path. Workers never see the RUN CONTRACT; this copy binds them.
4. `python3 method/gates/seq.py init --tier <tier>` creates `work/SCOREBOARD.md` (one row per requirement id with
its weight, one per deliverable folder of the tier). If it reports ids without a stated weight, look their
weights up in `INPUT/requirements.md` and correct those `weight` cells by hand.
5. Write `work/SKELETON-PLAN.md` from `method/templates/skeleton-plan.md` in at most 4 minutes (decide from what
you know; the workers research): the starting concept = the most conventional working principle that plausibly
meets every weight-4/5 threshold, two alternatives with different principles, the subsystem rows, the shared
names, and the owner (0-a, 0-b or 0-c) of every requirement id.
6. Write `work/briefs/0-a.md`, `0-b.md`, `0-c.md`: each holds the header and the done criteria of
`method/templates/increment-brief.md`, the ids the plan gives that worker, and the line "Body: section 0-a (0-b,
0-c) of method/templates/skeleton-briefs.md" (the worker reads it there). Then run the increment cycle (§5)
for increment 0 (concept tier: DEADLINE S+19).
## 3. Team and write rights
| who | does | may write |
|---|---|---|
| sequencer: you, the top session | plan, rank, brief, launch, integrate, check, report | `work/` (STATE.md, SKELETON-PLAN.md, BOUNDARIES.md, briefs/, SCOREBOARD.md through `seq.py` plus hand edits of its editable cells) and the ledger: `deliverables/REPORT.md`, `deliverables/manifest.json`, `deliverables/01-requirements/compliance.csv` (only through `seq.py merge` and `check_all.py --fix-quotes`), `deliverables/02-concept/increments.md` |
| increment-worker: one fresh subagent per brief, defined in `.claude/agents/increment-worker.md` (opus) | one increment, within its deadline | only its WRITE SCOPE inside `deliverables/`, plus `work/inc/<N>-<w>/` |
- Nobody writes `INPUT/`, `method/` or `.claude/`. Your only edit outside `work/` and the ledger is mechanical:
`check_all.py --fix-quotes` re-quotes numbers whose home moved by at most 10 %.
- At most three workers at once, and only with disjoint write scopes. Ledger files are never in a scope: every
increment changes requirement statuses, so a single writer (the merge script) is what lets workers run in
parallel without collisions.
- A fresh worker for every brief. Never hand a finished worker a new increment (it would carry its context
forward); the only follow-up messages a worker gets are its own retry and the wrap-up (§9).
- While an increment runs you do not edit anything under `deliverables/`; draft in `work/`.
## 4. Communication: through files only
- Down: the brief `work/briefs/<N>-<w>.md` (from `method/templates/increment-brief.md`: why, goal, ids in focus,
WRITE SCOPE, reading list, clock-time DEADLINE, done criteria). It carries no design knowledge that is not in the
package; if a worker would need it, it belongs in the package first.
- Up: `work/inc/<N>-<w>/claims.csv` (compliance rows plus evidence quality, risk and effort per id),
`work/inc/<N>-<w>/note.md` (written last: FIDELITY and PIVOT lines, then the change note) and the worker's final
message (the note again).
- Shared and read-only for workers: the package, `work/SCOREBOARD.md`, `work/BOUNDARIES.md`.
- Workers never talk to each other. No message leaves this run's session tree; no milestone messages are needed.
Tool calls:
- Launch every worker of an increment in ONE message, one call each:
`Agent({description: "increment 3-a", subagent_type: "increment-worker", prompt: "You are increment worker 3-a. Run directory: <absolute run dir>. Tier: <tier>. Read work/briefs/3-a.md and follow it. Write work/inc/3-a/note.md last and end with its text."})`
If the Agent tool offers a `name` field, set `name: "w3-a"`.
- Wait: `python3 method/gates/seq.py wait 3 --workers a,b --until <DEADLINE HH:MM>` with Bash timeout 300000
(it returns within 4 minutes). Exit 0: every note exists; 3: call it again; 2: the deadline passed (§9).
Notifications from background workers may arrive as well; the note files decide. If the Agent calls already
returned the workers' results, skip the wait.
- Retry: load SendMessage once (`ToolSearch({query: "select:SendMessage", max_results: 1})`), then
`SendMessage({to: "<name or agentId>", message: "Retry 3-a: check_all is RED: <error lines>. Fix it inside your scope (now also: <paths>; ids now also in focus: <ids>), re-run the check, rewrite claims.csv and note.md, end with the note."})`.
- Wrap-up, for a worker without a note at its DEADLINE: `SendMessage({to: "<name or agentId>", message: "Wrap up now: stop design work, make your files consistent, run the worker check, write claims.csv and note.md for what exists, end with the note."})`.
- Stop a worker: `TaskStop({task_id: "<name or agentId>"})` (load it the same way).
## 5. The increment cycle (every increment, in this order)
1. `date`. `python3 method/gates/seq.py rank` (from 60 % of the full-tier plan: `rank --late`). Choose by
`method/POLICY.md` §3: BLOCKING folders first, a pending pivot next, then the top candidates; at most 3 workers.
2. Write the briefs. A scope is a `;`-separated list of paths relative to `deliverables/`: a folder ends in `/`,
`02-concept/calc/<stem>.*` is one calculation (script, results, plots). Quotes move with their home in the
same increment: for every scope run `python3 method/gates/seq.py quoters "<scope>"` and add its ADD TO SCOPE
paths to the WRITE SCOPE and its IDS IN FOCUS to the ids in focus. With several workers,
`python3 method/gates/seq.py disjoint "<scope a>" "<scope b>" ...` must say yes; if not, bundle the overlapping
goals into one worker or leave one for the next increment.
3. `python3 method/gates/seq.py snap <N>`: the pre-increment copy (`work/snapshots/<N>/`). It says GREEN when the
last check (step 8) passed on exactly this package, and SKELETON while a folder of the tier is still empty: a
SKELETON increment is repaired forward, never reverted (§9).
4. Launch (§4); draft what REPORT.md will need in `work/`; wait (§4).
5. When every worker has returned: `python3 method/gates/seq.py changed <N> --scope "<scope a>" "<scope b>" ...`
with the scopes as briefed, as extended by a retry. Lines marked OUTSIDE SCOPE or LEDGER:
`python3 method/gates/seq.py revert <N> --outside "<scope a>" ...` (same scopes), and log it.
6. `python3 method/gates/seq.py merge <N>`: claims of the workers that wrote a note into `compliance.csv` and the
scoreboard. Act on its CONFLICT, REJECTED, SKIPPED, FIDELITY and PIVOT lines (REJECTED = RED for that worker,
SKIPPED = no note, §9).
7. Ledger: update `deliverables/REPORT.md` (handbook §15 order; when you first write it, put the lines
`<!-- STATUS-TABLE:BEGIN -->` and `<!-- STATUS-TABLE:END -->` where the status per requirement belongs in that
order; every number quoted from its home with a tag `<number> <unit> [=<key>@<path>]`, for example
`[=unit_cost.q1000@09-bom-cost/cost.json]`, re-read where a home moved; add a short "How this design was built"
paragraph pointing to `02-concept/increments.md`); run
`python3 method/gates/seq.py table --write deliverables/REPORT.md`; append the entry below to
`deliverables/02-concept/increments.md`; run `python3 method/gates/seq.py manifest --tier <tier>` and set
`key_figures` from the homes (null when unknown).
8. `python3 method/gates/check_all.py --tier <tier> --fix-quotes`. GREEN: update `work/STATE.md` (increment done,
clock, next step) and start the next cycle. RED: §9.
Entry in `increments.md` (no quote tags there; it is history):
```
## Increment N — HH:MM–HH:MM — GREEN | RETRIED | REVERTED
Chosen: <rank lines: id or folder, score, why now>
Workers: N-a <scope>; N-b <scope>
Changed: <files> · Numbers moved: <name old -> new (home)> · Status: <id old -> new>
Fidelity: <folder n> · Pivot: no | yes (<id, reason>) · Next: <what the scoreboard says now>
```
## 6. Concept tier
Contents of `deliverables/` at this tier:
- `01-requirements/requirements.md` (given ids with verification conditions, derived `DR-*` requirements,
conflicts, assumptions) and `compliance.csv` (one row per id, kept by the merge).
- `02-concept/`: `concept.md`; `alternatives.md` (three working principles, knock-out, weighted comparison,
sensitivity); `subsystems.csv` (a number for every subsystem: power, mass, envelope, cost range, key parts,
origin); `calc/` (the budget roll-up and the decisive estimates, each with its results file); `physics-map.md`;
`benchmark.md`; `safety.md`; `compliance-route.md`; `cost.md`; `risks.md`; `increments.md`.
- `09-bom-cost/cost.json` (rough; the home of the unit cost), `REPORT.md`, `manifest.json`.
How the method shows: by about S+23 all of the above exists in rough form and is GREEN; then typically 2–3
increments deepen exactly what the scoreboard ranks weakest (decisive calculations for risky numbers, a fair
alternative comparison, real parts and prices for the top cost items, hazards and the compliance route), each
ending GREEN and logged in `increments.md` with the rank lines that chose it. REPORT.md's status table shows the
evidence quality and risk of every requirement next to its status.
| clock | concept-tier work |
|---|---|
| S+0–6 | first steps, plan, increment-0 briefs, snapshot, launch |
| S+6–19 | increment 0: workers 0-a, 0-b, 0-c, DEADLINE S+19 |
| S+19–23 | integrate increment 0: the skeleton is GREEN |
| S+23–52 | increments 1…n: worker box at most 9 min (S 5, M 7, L 9), integration at most 3 min; last launch at S+40 |
| S+52–58 | final checks and finish (§10) |
Stop launching early when `rank` prints LOW (every folder at fidelity ≥ 2, no requirement scoring 6 or more). Do
not create `03`–`08`, `10` or `11` at this tier (the RUN CONTRACT excludes detailed design here);
`09-bom-cost/cost.json` is the only file outside `01` and `02`. After the final checks, finish (§10): this tier
stops at 01, 02, REPORT.md and manifest.json.
## 7. Full tier
Plan P = 8 h from START; `method/POLICY.md` §6 has the early stop and the single extension.
| clock | full-tier work |
|---|---|
| S+0–10 | first steps (§2) |
| S+10–30 | increment 0, front skeleton: 0-a, 0-b, 0-c, box 15 min |
| S+30–90 | increment 1, back skeleton: 1-a mechanical, 1-b electrical, 1-c numbers and documents, box 45 min; GREEN means all eleven folders at fidelity ≥ 1 |
| S+90 to 0.9 P | increments 2… by the scoreboard (boxes S 20 / M 40 / L 60 min, at most 3 workers); `python3 method/gates/seq.py prune --keep 4` after each GREEN increment |
| 0.9 P to P | the consistency increment, then the final (§10) |
Path to every deliverable (skeleton in increment 1, then increments until the handbook's quality bar):
- `03-cad/` (§7): envelope bodies from subsystems.csv → real part geometry, fits, zero interference, a process
per part; `assembly.step`, one STEP per designed part, the parametric scripts.
- `04-drawings/` (§8): block-level drawings → every manufactured part dimensioned and toleranced; an assembly
drawing whose balloons match `bom.csv`.
- `05-calculations/` (§6): the decisive scripts → every requirement-critical number by script or validated
simulation, each running as `ddpy <script>` in under 5 minutes.
- `06-electronics/` (§9): schematic of the key parts with ERC → complete schematic, routed board, ERC and DRC
reports, fabrication outputs.
- `07-firmware/` (§10): a building state-machine stub → fault handling, interlocks and their host tests.
- `08-safety-certification/` (§11): hazard table and rule map → complete hazard analysis, traced safety
requirements, conformity route, lab test list.
- `09-bom-cost/` (§12): major items with real parts → every line real; `cost.json` equal to the `bom.csv` sums;
`cost.md` at 1 / 100 / 1000.
- `10-prototype-test-plan/` (§13): one test per id → build plan, bring-up, every test with equipment and pass
criterion.
- `11-renders/` (§14): one view of the skeleton CAD → product, exploded and interface views of the final CAD, and
`assembly.glb`.
The consistency increment: one fresh worker, scope = every non-ledger file of the package, goal = find and fix
contradictions, stale numbers and dangling references; re-export renders and drawings if the CAD changed after
them; make `cost.json` equal the `bom.csv` sums.
## 8. Gates
- The check suite `method/gates/check_all.py` after every increment: GREEN, or the increment does not count. It
runs every calculation script with the toolchain Python, runs `method/gates/dd_check.py` (the RUN CONTRACT
formats) and adds the skeleton checks: required files, numbers in `subsystems.csv`, quote tags against their
homes, REPORT.md naming every requirement id, the manifest listing every file, cost totals within 1 %, full-tier
artefact headers, ERC and DRC reports.
- Scope gate (`seq.py changed`): out-of-scope and ledger edits by workers are reverted.
- Snapshot gate (`seq.py revert`): it returns only to a GREEN package and never undoes a skeleton.
- Merge gate (`seq.py merge`): claims with unknown ids, bad statuses or missing evidence are rejected.
- Pivot gate: `method/POLICY.md` §5, at most once per tier.
- Final gate: `python3 method/gates/dd_check.py --tier <tier> --run-scripts` with errors=0.
## 9. Failure handling
- RED after an increment: find the responsible worker from the paths in the error lines (for a stale quote: the
worker whose note moved that home). Rename its `note.md` to `note.try1.md`, send ONE retry (§4) with the error
lines (extend its scope only by the files that quote a home it moved, its focus by the ids whose value quotes
one), wait, then repeat cycle steps 5–8 with that worker's extended scope. Still RED:
- normal increment: `seq.py revert <N> --paths "<its scope>" --ledger`, `seq.py merge <N> --skip <w>`, redo steps
7–8. Still RED: `seq.py revert <N> --all`, log the increment as REVERTED and raise that candidate's effort one
step in the scoreboard.
- SKELETON increment (`revert` refuses it): keep every file. One fresh worker (next free letter, e.g. 0-d) whose
brief names only the failing files (scope = those files, goal = the error lines, box S), then steps 5–8 for
it. Still RED: make the smallest edits that pass
yourself (rough numbers labelled ASSUMPTION) and log them as a deviation in `increments.md`.
- No `note.md` at the DEADLINE: send that worker the wrap-up message (§4) and wait 3 more minutes (full tier 10).
Still none: `TaskStop` it and move `work/inc/<N>-<w>/` to `work/inc/stopped-<N>-<w>/` (merge skips a worker
without a note). Normal increment: `seq.py revert <N> --paths "<its scope>"`; one fresh worker with the same
brief and a new DEADLINE if a full box still ends before the final-check time, else go on without it.
SKELETON increment: keep its files; one fresh worker with the same brief, a new DEADLINE (box S) and the line
"Complete and repair what exists in your scope; do not start over"; nothing again: write the missing skeleton
files yourself (rough, labelled ASSUMPTION) and log it as a deviation.
- The Agent tool refuses `increment-worker`: use `subagent_type: "general-purpose"` with a prompt that begins
"Read .claude/agents/increment-worker.md (ignore its header) and act as that role." The Agent tool unusable: run
each worker as an Oivo session, `mcp__oivo__sessions({action: "spawn", task: "Read .claude/agents/increment-worker.md (ignore its header) and act as that role. You are increment worker 3-a. Run directory: <absolute run dir>. Tier: <tier>. Read work/briefs/3-a.md and follow it. Write work/inc/3-a/note.md last. Your parent never answers questions: decide yourself.",
directory: "<run dir>", title: "<run id = run dir name> W3-a", effortLevel: "max", autonomyMode: "unattended",
role: "developer", waitForSlot: 180})` (load the tool with ToolSearch), wait with `seq.py wait`, then
`mcp__oivo__sessions({action: "stop", session: "<sessionId>"})`. On `concurrency_limit`: wait at least 60 s
once, retry once, then use fewer workers and log it. Last resort only: do the increment yourself and log it as
a deviation in `increments.md`.
- A tool or route that fails: three honest attempts with new information, then a labelled fallback, and a
`blocked[]` entry if it mattered.
- Running out of time: never launch an increment whose box ends after the final-check time. The last GREEN
snapshot is the shippable package: if a RED cannot be fixed in time, `seq.py revert <it> --all` (`seq.py snaps`
marks it) and finish. Before the skeleton is GREEN there is none (`revert` refuses): make the smallest edits
that turn the check GREEN yourself, log them as a deviation, and finish.
## 10. Finishing
1. `python3 method/gates/check_all.py --tier <tier>` is GREEN and `python3 method/gates/dd_check.py --tier <tier>
--run-scripts` shows errors=0.
2. Final REPORT.md, re-read against the homes of its numbers (the full tier adds the file map); manifest synced,
`key_figures` set, `blocked[]` holding only what needs a human.
3. Nothing left running: every worker returned or stopped (`TaskStop`), every Oivo session you spawned stopped,
no background shells. Delete `work/snapshots/` if the disk budget is tight.
4. End with the final JSON report your Oivo contract asks for and `GUI_STATE` with `state: done`, as the RUN
CONTRACT says.
agents/increment-worker.md (110 wierszy)
---
name: increment-worker
description: Fresh worker for exactly ONE increment of a SKELETON hardware design run. Reads its brief file (path in the prompt), the package and the scoreboard; changes only the paths in its write scope; leaves the package consistent and checked; writes claims.csv and note.md in work/inc/<id>/; ends with its change note.
model: opus
effort: max
omitClaudeMd: true
---
# Increment worker
You do ONE increment of a hardware design run and then stop. You have no memory of earlier increments and
nobody will answer questions: the package in `deliverables/` is the memory. Whatever a later agent needs must
end up in the package or in your note, because the next agent starts as fresh as you. Your prompt gives the
run directory and your id `<N>-<w>` (for example `3-a`); work inside the run directory, with run-relative paths.
## 1. Read first (only what your brief needs)
1. `date` — your DEADLINE is a clock time in the brief.
2. Your brief `work/briefs/<N>-<w>.md`: goal, requirement ids in focus, WRITE SCOPE, deadline, done criteria.
3. `work/BOUNDARIES.md` (the run's boundaries, binding) and `method/HYGIENE.md` (ground rules, binding).
4. The handbook sections the brief names, in `.claude/skills/dd-hw-handbook/SKILL.md` (read the file).
5. `INPUT/brief.md`; in `INPUT/requirements.md` the entries of your ids (the whole file if the brief says so);
`INPUT/TOOLCHAIN.md` before you use any tool.
6. `deliverables/REPORT.md` (the front page of the package) and `work/SCOREBOARD.md` (read-only).
7. The files in your scope and the files they quote. `work/SKELETON-PLAN.md` only if the brief lists it.
## 2. Write rights
- Write only the paths in WRITE SCOPE (relative to `deliverables/`; a path ending in `/` is a folder, one ending
in `.*` every file of that stem, e.g. `02-concept/calc/power.*`) and your own folder `work/inc/<N>-<w>/`
(create it). Everything else is reverted automatically after the increment.
- Never write `INPUT/`, `method/`, `.claude/`, other `work/` files, or the ledger files `deliverables/REPORT.md`,
`deliverables/manifest.json`, `deliverables/01-requirements/compliance.csv`,
`deliverables/02-concept/increments.md` (the sequencer owns them; your claims reach compliance.csv by merge).
- A change you need outside your scope goes into note.md as a `Requests:` item; do not make it.
- Do not rename or delete a file that others cite as evidence or quote (`grep -rn "<file name>" deliverables/`
first). If one must move, claim the new path for the affected ids.
- Write every file whole. The run can be stopped at any moment; a half-written file is worse than an old
one. For a large rewrite write `work/inc/<N>-<w>/tmp/<name>` first, then move it into place.
- Never name a file `REPORT*`, `SUMMARY*`, `FINDINGS*` or `ANALYSIS*` (the harness refuses those for agents).
## 3. How to work
- Check `date` between steps. From DEADLINE minus 2 minutes, or at once when a message tells you to wrap up, stop
design work and do §4–§5. A smaller, consistent, checked change beats a bigger broken one; say what is left in
the note.
- Engineering rules: `method/HYGIENE.md` and the handbook. Estimate before you compute. Label every number
SOURCE / CALCULATION / SIMULATION / MEASUREMENT / ASSUMPTION. Part numbers, prices, standards and
competitor data only from pages you actually read (date in the text); a failed lookup becomes a labelled
ASSUMPTION range.
- Calculations: one standalone script per question in the calc folder of your scope (`02-concept/calc/` or
`05-calculations/`), no shared helper modules; inputs with their origin at the top; units with `pint`; fixed
random seeds; run it with `ddpy <script>` from its folder (offline, < 5 min); it writes
`<stem>.results.json` as `{"<name>": {"value": x, "unit": "..."}}`. Re-run a script after every edit.
- One home per number: a results file, `02-concept/subsystems.csv` or `09-bom-cost/cost.json`. Where you
quote a number elsewhere, put a quote tag right after it, in the home's unit, no thousands separators:
`1150 W [=peak_power@02-concept/calc/power_budget.results.json]` (a results name, a dotted key of a JSON file such
as `unit_cost.q1000@09-bom-cost/cost.json`, or `<row>.<column>` of a CSV whose first column names the rows,
such as `controller.power_w@02-concept/subsystems.csv`). The check suite compares every tag with its home.
- Quotes move with their home in the same increment. When you change a home value: `grep -rn "@<home path>"
deliverables/`; update every quote of it in your scope (the brief put the quoting files there; `--fix-quotes`
repairs moves of at most 10 %, re-read and rewrite the sentence for larger ones); add a claims row for every id
whose compliance value quotes it; list the move under "Numbers moved". A quote outside your scope: name the
file under "Requests". A move of more than 10 % there turns the increment RED and returns to you as a retry
with that file in your scope.
- Tools: Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch (load deferred ones with ToolSearch
`select:<Name>`), and the toolchain commands of `INPUT/TOOLCHAIN.md`. No installs. If a missing Python package
is truly needed, create a venv in your own folder (`python3 -m venv work/inc/<N>-<w>/venv`), install only
there and delete it when done; never modify `/opt/dd/venv` or the system Python (deliverable scripts must
still run with `ddpy` alone). Do not use Oivo MCP tools, do not start agents or sessions, do not message anyone. Keep build output
out of `deliverables/` (PlatformIO: delete `.pio/` after the build or set `build_dir` under
`work/inc/<N>-<w>/`).
- A tool that fails gets three honest attempts with new information each time, then a labelled fallback
(for example an analytical model instead of a simulation) and a line in the note.
- If your work shows that the chosen concept cannot meet the threshold of a requirement of weight 4 or 5 and
no fix exists inside its architecture, do not redesign outside your scope: write the PIVOT line (§5) with
the numbers.
## 4. Check before you finish
From the run directory: `python3 method/gates/check_all.py --tier <tier> --worker --scope "<your scope, ;-separated>"`
(add `--fix-quotes` to repair stale quotes inside your scope). Fix every ERROR. `LEDGER` and
`[outside your scope]` warnings belong to others; name in your note any that your change caused.
## 5. Outputs — write note.md last
1. `work/inc/<N>-<w>/claims.csv`, header `req_id,status,value,unit,evidence,note,quality,risk,effort`, one
row per requirement id your work addressed, including ids in focus you could not improve:
- `status` met / partly / not_met / not_addressed — at the concept tier a prediction from your numbers
(say "predicted" in `note`); a predicted miss is `not_met`, never hidden. not_addressed never replaces a
status that already exists.
- `value`, `unit` in the requirement's own metric and unit; `evidence` = path(s) inside `deliverables/`,
`;`-separated, that exist and show it; `note` = origin label and reason (a quote tag when `value` comes
from a home).
- `quality` none / estimate / calculation / simulation / sourced; `risk` H / M / L = could the current
concept fail this requirement; `effort` S / M / L = size of the next improvement for this id (method/POLICY.md §4).
2. `work/inc/<N>-<w>/note.md`:
```
FIDELITY <folder> <0-3> one line per deliverable folder you changed (scale: method/POLICY.md §4)
PIVOT no or: PIVOT yes <req_id>: <threshold, predicted value, why no fix inside this architecture>
## Change note <N>-<w>
Changed: <paths>
Numbers moved: <name old -> new unit (home)> | none
Decisions taken without a human: <decision — rejected alternatives — why> | none
Requests: <file outside my scope — change needed — why> | none
Open / next: <what the next increment on this topic should do first>
Check: <check_all --worker result; errors left and why>
```
3. Your final message is the text of note.md (at most 250 words) and nothing else.
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 (112 wierszy)
# POLICY — how this run decides without a human The sequencer applies this file; workers apply §1, §2 and §4. Every use of a default that changes the design is recorded where it matters, under "Decisions taken without a human", with the rejected alternatives. ## 1. Priorities (when anything conflicts, the higher one wins) 1. Safety of people and a product that may legally be sold. 2. Honest claims: a predicted miss is shown as a miss (`not_met`), never hidden or softened. 3. Anytime completeness: at every increment boundary the tier's package is complete, consistent and GREEN. Breadth before depth. 4. Thresholds of weight-5 requirements, then of weight-4. 5. Remaining thresholds, then targets, in weight order. 6. Unit cost and retail price. 7. Time and tokens: cut depth, never honesty or completeness. ## 2. Defaults for taste and judgment - Proven, conventional working principles unless a weight-4/5 requirement forces something new (then say why). - Buy before make for every subsystem that does not differentiate the product; pre-certified modules (radio, power supply) wherever they fit. - Safety by design first, then protective measures, then information. Every hazard that can hurt someone gets a protection that works without firmware. - Low voltage inside through a certified external supply when the power level and the requirements allow it; otherwise the supply route the requirements fix. - Manufacturing: 3D printing or CNC for 1 unit; the same with jigs for 100; moulding or stamping for 1000, tooling stated and amortised separately. - Currency EUR unless the requirements use another; prices net of VAT with VAT stated; SI units; mm in CAD. - Retail price from the benchmark; aim for a unit cost at 1000 of at most 40 % of the net retail price unless the requirements fix the price or margin. - A descriptive working name; no invented brand, trademark or styling claims; neutral appearance. - Conflicting requirements: safety first, then the higher weight, thresholds before targets; record the trade in `01-requirements/requirements.md`. - Uncertain inputs: compliance predictions use the pessimistic end of the range; say so in the note. - All deliverables in English. ## 3. Choosing the next increment 1. `seq.py rank` prints BLOCKING → the next increment fills those folders (the skeleton briefs do it). 2. A pivot is pending (§5) → the pivot increment comes next. 3. Otherwise take the top candidate. Add up to two more workers from the following candidates whose scopes do not overlap (`seq.py disjoint`); bundle up to three candidates that share files into one worker. Ties go to the lower-fidelity folder (rank already sorts so). The rank's grouping is a suggestion: widen a scope to every file its goal needs, never into a ledger file. Calculation scopes are file-level (`02-concept/calc/<stem>.*`: script, results, plots), so parallel calculation workers stay disjoint. 4. Quotes move with their home, inside the same increment: every scope gets what `seq.py quoters` lists (files quoting a home in it, scripts reading a file in it; ids whose compliance value quotes it go into the focus). If that makes scopes overlap, bundle or postpone; a stale quote never crosses an increment boundary (check_all is RED on it). Worker `Requests:` become goals of the next increment that owns those files, or of a small increment of their own. 5. Size each worker by the effort of its candidates: concept tier S 5 / M 7 / L 9 minutes, full tier S 20 / M 40 / L 60 minutes. A goal larger than L is split. 6. From 60 % of the full-tier plan use `seq.py rank --late`, so no folder ends below fidelity 2. ## 4. Scales - Evidence quality: `none`; `estimate` (hand estimate or rule of thumb with its origin); `calculation` (a script with its results file); `simulation` (a solver result with a known-answer check); `sourced` (a datasheet, standard or page actually read, with URL and date). - Risk — can the current concept fail this requirement? `H`: margin to the threshold under 10 % or unknown on a weight-4/5 id, or predicted `not_met`/`partly`; `M`: margin 10–30 %, or estimate-only evidence; `L`: margin over 30 % with calculation or sourced evidence. - Effort of the next improvement: `S` one lookup or one short script; `M` a few scripts or a document section; `L` a model, a simulation or several files. - Folder fidelity: `0` missing; `1` skeleton (the files exist, are truthful, rough numbers are labelled, they fit each other); `2` credible (decisive numbers by calculation or source, real parts for the key items, consistent with the neighbouring folders); `3` detailed (the handbook's quality bar for that folder is met). ## 5. Pivot rule (at most once per tier) - Trigger: a worker's `PIVOT yes` line or a `PIVOT CHECK` line of `seq.py rank`, where the numbers show that the chosen concept misses the threshold of a weight-4/5 requirement and no fix exists inside its architecture (a parameter change, another part or a local redesign of one subsystem would be a fix — then schedule that instead). - Action: the next increment is the pivot. Wave 1: one fresh worker with scope `02-concept/alternatives.md`, `02-concept/concept.md`, `02-concept/subsystems.csv`, `02-concept/calc/`, `02-concept/risks.md` plus what `seq.py quoters` adds, and the goal "re-run the comparison with this fact as a knock-out; keep or switch; record why". Keep: integrate and check as usual. Switch: before the check, wave 2 re-runs the skeleton bodies of the files built on the old concept against the new one, each with its quoters (concept tier: 0-c; full tier: 1-a, 1-b, and 1-c with the 0-c files added); one snapshot and one check for both waves; the folders concerned go to fidelity 1. Launch a pivot only if both waves end before the final-check time; otherwise the miss stays `not_met` and REPORT.md names the alternative that would meet it. - Then write `pivot: used (increment N, <id>)` in `work/SCOREBOARD.md`. After that a miss stays `not_met` with its best mitigation, its scoreboard note becomes `ACCEPTED miss: <reason>`, and REPORT.md states it in the first paragraph. ## 6. Time plan and stop rules (S = START from `date`) - Concept tier: increment 0 launched by S+6 with DEADLINE S+19; skeleton GREEN by about S+23; then typically 2–3 increments of at most 12 minutes wall time; the last launch at S+40, also when increment 0 overran; final checks from S+52 (or right after the last increment); done by S+58. If the skeleton is not GREEN by S+35, launch no increment: make it GREEN by repairing forward (MAIN.md §9) and finish. - Full tier: plan P = 8 h. Front skeleton (increment 0) by S+30, back skeleton (increment 1) GREEN by S+90; no new increment after 0.9 P except the consistency increment; done by P. One extension of at most 2 h, only when a weight-5 requirement is `not_met` or `partly` and a ranked increment can fix it (log it in `increments.md`). Never plan past S+12 h. - Early stop (less time is better, RUN CONTRACT §4): `rank` prints LOW when every folder is at fidelity ≥ 2 and no requirement scores 6 or more. Concept tier: stop launching and finish. Full tier: when LOW and every weight-4/5 id has calculation-or-better evidence, the consistency increment and the final. - Check `date` before every launch; never launch an increment whose box ends after the final-check time. ## 7. Retry, revert, blocked - RED → one retry of the responsible worker → revert its scope and re-merge without it → revert the whole increment. A reverted candidate keeps its scoreboard row with effort raised one step and a note. - A skeleton increment (`seq.py snap` says SKELETON) is never reverted: RED after the retry → one fresh worker for the failing files → the sequencer's own smallest edits, logged as a deviation. - A worker without a note at its DEADLINE → the wrap-up message → 3 minutes (full tier 10) → stop it; its claims are not merged. Normal increment: revert its scope, one fresh worker if a full box still fits. Skeleton increment: keep its files, one fresh worker to complete them (box S), then the sequencer's own edits. - `manifest.json` → `blocked[]` only for what truly needs a human (a purchase, a physical test, a credential, a legal opinion) or a tool that failed after three attempts where it mattered. The rest of the work continues.
files/method/gates/check_all.py (460 wierszy)
#!/usr/bin/env python3
"""check_all.py - the SKELETON check suite: GREEN (exit 0) or RED (exit 1). Standard library only.
python3 method/gates/check_all.py --tier concept|full [--worker --scope "P1;P2"] [--no-run] [--fix-quotes]
Run from the run directory: the sequencer after every increment, every worker before it finishes.
1. Runs every .py under deliverables/02-concept/ and 05-calculations/ with the toolchain Python from its own
folder (<= 300 s each, in parallel; a worker re-runs only those in its scope): it must exit 0 and write
<stem>.results.json; values that change on re-run are reported (a script was edited without being re-run).
2. Runs method/gates/dd_check.py (the formats the RUN CONTRACT fixes).
3. Skeleton checks: required files (concept tier: every increment-0 file but benchmark, physics map and risks);
02-concept/subsystems.csv (a number for every subsystem); quote tags [=key@path] in .md files and in the
note column of compliance.csv; REPORT.md names every requirement id;
manifest.json lists every file; cost.json vs subsystems, bom.csv (full tier, 1 %) and key_figures (1 %);
full tier: STEP/PDF/SVG headers, ERC/DRC reports, platformio.ini [env:]; disk use.
--worker: ledger files (REPORT.md, manifest.json, 01-requirements/compliance.csv, 02-concept/increments.md) and,
with --scope, files outside the scope give WARN instead of ERROR. --fix-quotes rewrites a stale quoted number
when its home moved by <= 10 % (a worker only inside its scope); larger moves stay ERROR.
The sequencer's check (no --worker, no --no-run) records GREEN/RED and the package digest in work/last-check.json;
seq.py snap reads it.
Quote tag: "1150 W [=peak_power@02-concept/calc/power_budget.results.json]": the number right before the tag must
equal the home value (a results name, a dotted JSON key, or <row>.<column> of a CSV whose first column names
rows) within half the last shown digit or 1 %, whichever is larger.
"""
import argparse
import concurrent.futures
import csv
import io
import json
import os
import re
import subprocess
import sys
import time
sys.dont_write_bytecode = True
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import dd_check as ddc # noqa: E402
import seq # noqa: E402 one definition of scopes, ledger files, quote tags and the package digest
LEDGER, JUNK, TAG, norm, under = seq.LEDGER, seq.JUNK, seq.TAG, seq.norm, seq.under
SUBSYS = ["subsystem", "function", "key_parts", "power_w", "mass_g", "envelope_mm", "cost_low", "cost_high",
"currency", "origin", "source"]
ORIGINS = {"SOURCE", "CALCULATION", "SIMULATION", "MEASUREMENT", "ASSUMPTION"}
REQUIRED = {"both": ["01-requirements/requirements.md", "02-concept/concept.md", "02-concept/alternatives.md",
"02-concept/subsystems.csv", "02-concept/increments.md"],
"concept": ["02-concept/cost.md", "02-concept/safety.md", "02-concept/compliance-route.md",
"09-bom-cost/cost.json"]}
PLANNED = ["benchmark.md", "physics-map.md", "risks.md", "safety.md", "compliance-route.md", "cost.md"]
NUM = re.compile(r"(?<![A-Za-z0-9_.])[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?")
BUDGET = {"concept": 0.5e9, "full": 4e9}
class Findings:
def __init__(self, worker, scopes):
self.items, self.worker, self.scopes = [], worker, scopes
def add(self, level, path, msg, ledger=False):
path = norm(path) if path else None
if level == "ERROR" and self.worker:
if ledger or path in LEDGER:
level, msg = "WARN", "LEDGER " + msg
elif self.scopes is not None and path and not any(under(path, s) for s in self.scopes):
level, msg = "WARN", "[outside your scope] " + msg
self.items.append((level, msg))
def may_write(self, rel):
return not self.worker or (rel not in LEDGER and self.scopes is not None and
any(under(rel, s) for s in self.scopes))
def files(folder, exts=None, junk=False):
out = []
for root, dirs, names in os.walk(folder):
dirs[:] = [d for d in dirs if junk or d not in JUNK]
out += [os.path.join(root, n) for n in sorted(names) if os.path.isfile(os.path.join(root, n)) and
os.path.getsize(os.path.join(root, n)) > 0 and (not exts or n.lower().endswith(tuple(exts)))]
return out
def jload(path):
try:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
except Exception: # noqa: BLE001
return None
def number(x):
x = x.get("value") if isinstance(x, dict) else x
return float(x) if isinstance(x, (int, float)) and not isinstance(x, bool) else None
def run_scripts(deliv, fx, run):
scripts = files(os.path.join(deliv, "02-concept"), (".py",)) + files(os.path.join(deliv, "05-calculations"), (".py",))
env = dict(os.environ, MPLBACKEND="Agg")
def one(script):
res = os.path.splitext(script)[0] + ".results.json"
before = jload(res)
if not run or not fx.may_write(os.path.relpath(script, deliv).replace(os.sep, "/")):
return script, res, before, 0, "" # a worker re-runs only the scripts of its own scope
try:
p = subprocess.run([ddc.toolchain_python(), os.path.basename(script)], cwd=os.path.dirname(script),
env=env, capture_output=True, text=True, timeout=300)
return script, res, before, p.returncode, p.stderr.strip()[-300:]
except subprocess.TimeoutExpired:
return script, res, before, "timeout", ""
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool:
for script, res, before, code, err in pool.map(one, scripts):
rel, rres = os.path.relpath(script, deliv), os.path.relpath(res, deliv)
after = jload(res)
if code != 0:
fx.add("ERROR", rel, f"{rel} {'runs longer than 300 s' if code == 'timeout' else f'exits {code}: {err}'}")
elif not os.path.isfile(res):
fx.add("ERROR", rel, f"{rel} writes no {os.path.basename(res)}")
elif not isinstance(after, dict):
fx.add("ERROR", rel, f"{rres} is not a JSON object")
else:
bad = [k for k, v in after.items() if not (isinstance(v, dict) and {"value", "unit"} <= set(v))]
if bad:
fx.add("WARN", rel, f"{rres}: entries without value/unit: {bad[:5]}")
if run and isinstance(before, dict):
moved = [f"{k} {before.get(k, {}).get('value') if isinstance(before.get(k), dict) else before.get(k)}"
f"->{after.get(k, {}).get('value') if isinstance(after.get(k), dict) else after.get(k)}"
for k in sorted(set(before) | set(after)) if before.get(k) != after.get(k)]
if moved:
fx.add("WARN", rel, f"{rres} changed on re-run (now refreshed): {', '.join(moved[:5])} — the "
"script was edited without a re-run; quotes of these values may be stale")
return len(scripts)
def run_dd_check(root, tier, fx):
try:
p = subprocess.run([sys.executable, ddc.__file__, "--tier", tier, "--run-dir", root, "--json"],
capture_output=True, text=True, timeout=900)
data = json.loads(p.stdout.strip().splitlines()[-1])
except Exception as exc: # noqa: BLE001
fx.add("ERROR", None, f"dd_check.py did not run: {exc}")
return
for level, key in (("ERROR", "errors"), ("WARN", "warnings"), ("NOTE", "notes")):
for msg in data.get(key, []):
if level == "WARN" and ("not listed in manifest.json" in msg or ("has no" in msg and ".results.json" in msg)):
continue # check_all reports these itself, as errors
m = re.search(r"((?:deliverables/)?\d\d-[\w.-]+(?:/[\w.-]+)*/?)", msg)
path = m.group(1) if m else next((f"09-bom-cost/{n}" for n in ("cost.json", "bom.csv")
if re.search(rf"(?<![\w/.-]){re.escape(n)}\b", msg)), None)
if path is None and level == "ERROR" and fx.worker and fx.scopes is not None:
level, msg = "WARN", "[not tied to a file in your scope] " + msg
fx.add(level, path, "dd_check: " + msg,
ledger=any(k in msg for k in ("compliance.csv", "manifest.json", "REPORT.md", "key_figures", "blocked[]")))
def check_skeleton(deliv, fx, man, tier):
required = REQUIRED["both"] + (REQUIRED["concept"] if tier == "concept" else [])
for rel in required:
if not os.path.isfile(os.path.join(deliv, rel)) or not os.path.getsize(os.path.join(deliv, rel)):
fx.add("ERROR", rel, f"{rel} missing or empty (skeleton file)")
for name in PLANNED:
if "02-concept/" + name not in required and not os.path.isfile(os.path.join(deliv, "02-concept", name)):
fx.add("WARN", "02-concept/" + name, f"02-concept/{name} missing (planned file; fine if its content lives elsewhere)")
path, rel = os.path.join(deliv, "02-concept", "subsystems.csv"), "02-concept/subsystems.csv"
if not os.path.isfile(path):
return None
rows = list(csv.reader(io.StringIO(open(path, encoding="utf-8-sig", errors="replace").read())))
if not rows or [h.strip().lower() for h in rows[0]] != SUBSYS:
fx.add("ERROR", rel, f"{rel} header must be exactly {','.join(SUBSYS)}")
return None
data = [(n, r) for n, r in enumerate(rows[1:], 2) if any(c.strip() for c in r)]
if len(data) < 3:
fx.add("ERROR", rel, f"{rel} has {len(data)} rows; the skeleton needs a row for every subsystem (>= 3)")
names, tot, cur = set(), dict.fromkeys(("power_w", "mass_g", "cost_low", "cost_high"), 0.0), set()
for n, row in data:
if len(row) != len(SUBSYS):
fx.add("ERROR", rel, f"{rel} line {n}: {len(row)} fields, expected {len(SUBSYS)} (quote fields with commas)")
continue
r = dict(zip(SUBSYS, (c.strip() for c in row)))
if not r["subsystem"] or r["subsystem"] in names:
fx.add("ERROR", rel, f"{rel} line {n}: subsystem name empty or duplicate (names are quote keys)")
names.add(r["subsystem"])
for col in tot:
try:
v = float(r[col])
assert v >= 0
tot[col] += v
except (ValueError, AssertionError):
fx.add("ERROR", rel, f"{rel} line {n} ({r['subsystem']}): {col} '{r[col]}' is not a number >= 0")
if num_ok(r["cost_low"]) and num_ok(r["cost_high"]) and float(r["cost_low"]) > float(r["cost_high"]):
fx.add("ERROR", rel, f"{rel} line {n}: cost_low > cost_high")
if not re.match(r"^\d+(\.\d+)?\s*[x×]\s*\d+(\.\d+)?\s*[x×]\s*\d+(\.\d+)?$", r["envelope_mm"]):
fx.add("WARN", rel, f"{rel} line {n}: envelope_mm '{r['envelope_mm']}' is not LxWxH")
labels = [t.upper() for t in re.split(r"[/+,;& ]+", r["origin"]) if t]
if not labels or not set(labels) <= ORIGINS:
fx.add("ERROR", rel, f"{rel} line {n}: origin '{r['origin']}' must use {sorted(ORIGINS)}")
cur.add(r["currency"])
fx.add("NOTE", rel, f"subsystems: {len(data)}; power {tot['power_w']:g} W; mass {tot['mass_g'] / 1000:.3g} kg; "
f"cost {tot['cost_low']:g}-{tot['cost_high']:g} {'/'.join(sorted(c for c in cur if c))}")
mass = number(((man or {}).get("key_figures") or {}).get("mass_kg")) if isinstance(man, dict) else None
if mass and tot["mass_g"] and abs(mass - tot["mass_g"] / 1000) / mass > 0.30:
fx.add("WARN", "manifest.json", f"key_figures.mass_kg {mass:g} vs subsystem sum {tot['mass_g'] / 1000:.3g} kg (> 30 %)")
return tot
def num_ok(x):
try:
float(x)
return True
except ValueError:
return False
class Homes:
def __init__(self, deliv):
self.deliv, self.cache = deliv, {}
def value(self, key, home):
rel = norm(home)
if rel not in self.cache:
p = os.path.join(self.deliv, rel)
self.cache[rel] = None if not os.path.isfile(p) else jload(p) if rel.endswith(".json") else \
list(csv.reader(io.StringIO(open(p, encoding="utf-8-sig", errors="replace").read())))
data, key = self.cache[rel], key.strip()
if data is None:
return None, f"home {rel} missing or unreadable"
if rel.endswith(".json"):
node = data.get(key) if isinstance(data, dict) and key in data else data
if node is data:
for part in key.split("."):
ok_d, ok_l = isinstance(node, dict) and part in node, isinstance(node, list) and part.isdigit()
if not (ok_d or ok_l and int(part) < len(node)):
return None, f"key '{key}' not in {rel}"
node = node[part] if ok_d else node[int(part)]
v = number(node)
return (v, None) if v is not None else (None, f"'{key}' in {rel} is not a number")
rowkey, _s, col = key.rpartition(".")
hdr = [h.strip() for h in data[0]] if data else []
row = next((r for r in data[1:] if r and r[0].strip() == rowkey), None)
if col not in hdr or row is None or not num_ok(row[hdr.index(col)] if hdr.index(col) < len(row) else ""):
return None, f"'{key}' (row.column) not found as a number in {rel}"
return float(row[hdr.index(col)]), None
def stale(shown, home):
dec = len(shown.split(".")[1]) if "." in shown and "e" not in shown.lower() else 0
return abs(float(shown) - home) > max(0.5 * 10 ** -dec, 0.01 * abs(home)) * (1 + 1e-9)
def fmt(home, shown):
dec = len(shown.split(".")[1]) if "." in shown and "e" not in shown.lower() else 0
if "e" in shown.lower() or (not dec and abs(home) < 10):
return f"{home:.3g}"
return f"{home:.{dec}f}" if dec else str(int(round(home)))
def compare_quote(fx, rel, where, txt, val, key, home, fix, apply):
if not stale(txt, val):
return False
moved = abs(val - float(txt)) / max(abs(float(txt)), 1e-12)
if fix and fx.may_write(rel) and moved <= 0.10:
apply(fmt(val, txt))
fx.add("NOTE", rel, f"FIXED {where}: {txt} -> {fmt(val, txt)} ({key}@{home})")
return True
if moved <= 0.10:
hint = "not in your write scope" if fix else "--fix-quotes repairs it"
elif fx.worker:
hint = "> 10 %: re-read the sentence and rewrite it"
elif rel in ("REPORT.md", "02-concept/increments.md"):
hint = "> 10 %: rewrite it yourself (ledger)"
else:
hint = "> 10 %: retry the worker that moved the home, with this file in scope or this id in focus (MAIN.md §9)"
fx.add("ERROR", rel, f"{where}: stale quote {txt}, home {key}@{home} = {val:g} (moved {moved * 100:.0f} %; {hint})")
return False
def check_quotes(deliv, fx, fix):
homes, total = Homes(deliv), 0
for path in files(deliv, (".md",)):
rel = os.path.relpath(path, deliv).replace(os.sep, "/")
if rel == "02-concept/increments.md":
continue
lines, fence, changed = open(path, encoding="utf-8", errors="replace").read().split("\n"), False, False
for i, line in enumerate(lines):
if line.strip().startswith("```"):
fence = not fence
if fence or line.strip().startswith("```"):
continue
blank = TAG.sub(lambda t: " " * len(t.group(0)), line).replace("\N{MINUS SIGN}", "-")
for m in reversed(list(TAG.finditer(line))):
total += 1
val, err = homes.value(m.group(1), m.group(2))
lo = max(0, m.start() - 48)
nums = list(NUM.finditer(blank[lo:m.start()]))
if err or not nums:
fx.add("ERROR", rel, f"{rel}:{i + 1}: quote tag [={m.group(1)}@{m.group(2)}]: {err or 'no number right before it'}")
continue
n = nums[-1]
a, b = lo + n.start(), lo + n.end()
def put(new, a=a, b=b, i=i):
lines[i] = lines[i][:a] + new + lines[i][b:]
changed |= compare_quote(fx, rel, f"{rel}:{i + 1}", n.group(0), val, m.group(1), m.group(2), fix, put)
if changed:
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines))
comp, crel = os.path.join(deliv, "01-requirements", "compliance.csv"), "01-requirements/compliance.csv"
if os.path.isfile(comp):
rows, changed = list(csv.reader(io.StringIO(open(comp, encoding="utf-8-sig", errors="replace").read()))), False
for row in rows[1:]:
for m in TAG.finditer(row[5] if len(row) > 5 else ""):
total += 1
val, err = homes.value(m.group(1), m.group(2))
if err:
fx.add("ERROR", crel, f"compliance.csv {row[0]}: quote tag: {err}")
elif NUM.fullmatch(row[2].strip()):
changed |= compare_quote(fx, crel, f"compliance.csv {row[0]} value", row[2].strip(), val, m.group(1),
m.group(2), fix, lambda new, row=row: row.__setitem__(2, new))
if changed:
buf = io.StringIO()
csv.writer(buf, lineterminator="\n").writerows(rows)
open(comp, "w", encoding="utf-8", newline="").write(buf.getvalue())
fx.add("NOTE", None, f"quote tags checked: {total}")
def check_ledger(deliv, ids, fx, man, tier):
report = os.path.join(deliv, "REPORT.md")
if os.path.isfile(report):
text = open(report, encoding="utf-8", errors="replace").read()
missing = [i for i in ids if i not in text]
if missing:
fx.add("ERROR", "REPORT.md", f"REPORT.md lacks {len(missing)} requirement id(s) ({', '.join(missing[:8])}) — "
"seq.py table --write deliverables/REPORT.md")
if not TAG.search(text):
fx.add("WARN", "REPORT.md", "REPORT.md quotes no number with a [=key@path] tag")
if isinstance(man, dict):
listed = {norm(f.get("path")) for f in man.get("files", []) if isinstance(f, dict)}
unlisted, junk = [], []
for p in files(deliv, junk=True):
rel = os.path.relpath(p, deliv).replace(os.sep, "/")
if any(s in JUNK or s.endswith("-backups") for s in rel.split("/")[:-1]) or rel.endswith((".pyc", ".tmp", "~")):
junk.append(rel)
elif rel != "manifest.json" and rel not in listed and not any(under(rel, l) for l in listed if l):
unlisted.append(rel)
if unlisted:
fx.add("ERROR", "manifest.json", f"manifest.json lacks {len(unlisted)} file(s), e.g. {unlisted[:4]} — "
f"seq.py manifest --tier {tier}")
if junk:
fx.add("WARN", junk[0], f"{len(junk)} build/cache file(s) in deliverables/ (delete them), e.g. {junk[:3]}")
def check_costs(deliv, fx, man, tier, tot):
cost = jload(os.path.join(deliv, "09-bom-cost", "cost.json"))
if not isinstance(cost, dict):
return
bom_q1000 = number((cost.get("bom") or {}).get("q1000")) if isinstance(cost.get("bom"), dict) else None
if tot and bom_q1000 and tot["cost_high"] and not 0.8 * tot["cost_low"] <= bom_q1000 <= 1.25 * tot["cost_high"]:
fx.add("WARN", "09-bom-cost/cost.json", f"cost.json bom.q1000 {bom_q1000:g} lies outside the subsystem cost "
f"range {tot['cost_low']:g}-{tot['cost_high']:g}: reconcile")
bom = os.path.join(deliv, "09-bom-cost", "bom.csv")
if tier == "full" and os.path.isfile(bom) and isinstance(cost.get("bom"), dict):
rows = list(csv.DictReader(io.StringIO(open(bom, encoding="utf-8-sig", errors="replace").read())))
if len({r.get("currency", "").strip() for r in rows}) == 1:
for q in ("1", "100", "1000"):
try:
s = sum(float(r["qty"]) * float(r[f"unit_price_{q}"]) for r in rows)
except (KeyError, TypeError, ValueError):
continue
ref = number(cost["bom"].get(f"q{q}"))
if ref and abs(ref - s) / ref > 0.01:
fx.add("ERROR", "09-bom-cost/cost.json", f"cost.json bom.q{q}={ref:g} vs bom.csv sum {s:.2f} (> 1 %)")
uc = ((man or {}).get("key_figures") or {}).get("unit_cost") if isinstance(man, dict) else None
if isinstance(uc, dict) and isinstance(cost.get("unit_cost"), dict):
for q in ("q1", "q100", "q1000"):
a, b = number(uc.get(q)), number(cost["unit_cost"].get(q))
if a is not None and b and abs(a - b) / b > 0.01:
fx.add("ERROR", "manifest.json", f"key_figures.unit_cost.{q}={a:g} but cost.json unit_cost.{q}={b:g}")
def check_full(deliv, fx):
for p in files(os.path.join(deliv, "03-cad"), (".step", ".stp")):
if not open(p, "rb").read(256).lstrip().startswith(b"ISO-10303-21"):
fx.add("ERROR", os.path.relpath(p, deliv), f"{os.path.relpath(p, deliv)} is not a STEP file")
for p in files(os.path.join(deliv, "04-drawings"), (".pdf", ".svg")):
head = open(p, "rb").read(4096)
if not (head.startswith(b"%PDF") if p.lower().endswith(".pdf") else b"<svg" in head.lower()):
fx.add("ERROR", os.path.relpath(p, deliv), f"{os.path.relpath(p, deliv)} does not open as PDF/SVG")
ele = files(os.path.join(deliv, "06-electronics"))
names = [os.path.basename(p).lower() for p in ele]
if ele and not any("erc" in n for n in names):
fx.add("ERROR", "06-electronics", "06-electronics/ has no ERC report (kicad-cli sch erc)")
if any(n.endswith(".kicad_pcb") for n in names) and not any("drc" in n for n in names):
fx.add("ERROR", "06-electronics", "06-electronics/ has a board but no DRC report (kicad-cli pcb drc)")
for p in ele:
n = os.path.basename(p).lower()
if n.endswith(".json") and ("erc" in n or "drc" in n):
d = jload(p) or {}
v = [x for s in d.get("sheets", []) for x in s.get("violations", [])] + \
[x for k in ("violations", "unconnected_items", "schematic_parity") for x in d.get(k, []) or []]
errs = sum(isinstance(x, dict) and x.get("severity") == "error" for x in v)
if errs:
fx.add("WARN", os.path.relpath(p, deliv), f"{os.path.relpath(p, deliv)}: {errs} error(s) — fix or justify each waiver")
ini = os.path.join(deliv, "07-firmware", "platformio.ini")
if os.path.isfile(ini) and "[env:" not in open(ini, encoding="utf-8", errors="replace").read():
fx.add("ERROR", "07-firmware/platformio.ini", "07-firmware/platformio.ini has no [env:...] section")
def main():
ap = argparse.ArgumentParser(description="SKELETON check suite (GREEN/RED); see the module docstring.")
ap.add_argument("--tier", choices=["concept", "full"], required=True)
ap.add_argument("--run-dir", default=".")
ap.add_argument("--worker", action="store_true")
ap.add_argument("--scope", nargs="*")
ap.add_argument("--no-run", action="store_true")
ap.add_argument("--fix-quotes", action="store_true")
a = ap.parse_args()
t0, root = time.time(), os.path.abspath(a.run_dir)
deliv = os.path.join(root, "deliverables")
scopes = [norm(c) for s in a.scope for c in s.replace(",", ";").split(";") if c.strip()] if a.scope else None
fx = Findings(a.worker, scopes)
if not os.path.isdir(deliv):
fx.add("ERROR", None, "deliverables/ missing")
else:
n = run_scripts(deliv, fx, not a.no_run)
run_dd_check(root, a.tier, fx)
man = jload(os.path.join(deliv, "manifest.json"))
tot = check_skeleton(deliv, fx, man, a.tier)
check_quotes(deliv, fx, a.fix_quotes)
check_ledger(deliv, ddc.requirement_ids(os.path.join(root, "INPUT", "requirements.md")), fx, man, a.tier)
check_costs(deliv, fx, man, a.tier, tot)
if a.tier == "full":
check_full(deliv, fx)
seen, size = set(), 0
for r, _d, names in os.walk(root):
for nm in names:
st = os.lstat(os.path.join(r, nm))
if (st.st_dev, st.st_ino) not in seen:
seen.add((st.st_dev, st.st_ino))
size += st.st_size
share = size / BUDGET[a.tier]
fx.add("WARN" if share > 0.7 else "NOTE", None, f"run directory {size / 1e6:.0f} MB = {share * 100:.0f} % of the "
f"{a.tier} budget{' — seq.py prune, delete scratch' if share > 0.7 else ''}; {n} script(s)"
f"{' checked' if a.no_run else ' run'}")
for level in ("NOTE", "WARN", "ERROR"):
for lv, msg in fx.items:
if lv == level:
print(f"{level:5} {msg}")
errors = sum(lv == "ERROR" for lv, _m in fx.items)
print(f"check_all: {'RED' if errors else 'GREEN'} tier={a.tier}{' (worker view)' if a.worker else ''} errors={errors} "
f"warnings={sum(lv == 'WARN' for lv, _m in fx.items)} in {time.time() - t0:.0f} s")
if not a.worker and not a.no_run and os.path.isdir(deliv): # what seq.py snap calls a GREEN package
os.makedirs(os.path.join(root, "work"), exist_ok=True)
digest = seq.tree_digest({r: seq.sha(p) for r, p in seq.walk(deliv).items()})
with open(os.path.join(root, "work", "last-check.json"), "w", encoding="utf-8") as fh:
json.dump({"time": time.strftime("%H:%M:%S"), "tier": a.tier, "status": "RED" if errors else "GREEN",
"errors": errors, "digest": digest}, fh)
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
files/method/gates/dd_check.py (431 wierszy)
#!/usr/bin/env python3
"""dd_check.py - check deliverables/ against the RUN CONTRACT layout (contract v1.x).
Usage (from the run directory):
python3 method/gates/dd_check.py --tier concept|full [--run-scripts] [--json]
Checks the formats the RUN CONTRACT fixes (compliance.csv, manifest.json, bom.csv, cost.json, the
folder layout of the tier, evidence paths) and prints ERROR / WARN lines. Exit code 0 = no errors.
It checks form, not engineering quality. Standard library only.
"""
import argparse
import csv
import io
import json
import os
import re
import shutil
import subprocess
import sys
COMPLIANCE_HEADER = ["req_id", "status", "value", "unit", "evidence", "note"]
COMPLIANCE_STATUS = {"met", "partly", "not_met", "not_addressed"}
BOM_HEADER = ["item", "qty", "description", "manufacturer", "mpn", "supplier", "supplier_part", "url",
"unit_price_1", "unit_price_100", "unit_price_1000", "currency", "notes"]
HEADING_ID = re.compile(r"^#{1,6}\s+([A-Z][A-Z0-9]{1,7}-[A-Z0-9]{1,7}-\d{1,4})\b")
LOOSE_ID = re.compile(r"\b([A-Z][A-Z0-9]{1,7}-[A-Z]{2,7}-\d{2,4})\b")
FULL_DIRS = ["03-cad", "04-drawings", "05-calculations", "06-electronics", "07-firmware",
"08-safety-certification", "09-bom-cost", "10-prototype-test-plan", "11-renders"]
class Report:
def __init__(self):
self.errors, self.warnings, self.notes = [], [], []
def error(self, msg):
self.errors.append(msg)
def warn(self, msg):
self.warnings.append(msg)
def note(self, msg):
self.notes.append(msg)
def requirement_ids(path):
"""Ids of INPUT/requirements.md: headings first, loose tokens as a fallback."""
if not os.path.isfile(path):
return []
with open(path, encoding="utf-8", errors="replace") as fh:
text = fh.read()
ids = []
for line in text.splitlines():
m = HEADING_ID.match(line.strip())
if m and m.group(1) not in ids:
ids.append(m.group(1))
if not ids:
for m in LOOSE_ID.finditer(text):
if m.group(1) not in ids:
ids.append(m.group(1))
return ids
def nonempty_files(folder, exts=None):
out = []
for root, _dirs, files in os.walk(folder):
for name in files:
p = os.path.join(root, name)
if os.path.getsize(p) == 0:
continue
if exts and not name.lower().endswith(tuple(exts)):
continue
out.append(p)
return out
def split_paths(field):
parts = re.split(r"[;|\n]+|\s+(?=[\w./-]+/)", field or "")
return [p.strip().strip("`'\"") for p in parts if p.strip()]
def evidence_exists(deliv, ref):
ref = ref.split("#", 1)[0].split("::", 1)[0].strip()
if not ref:
return True
if ref.startswith("deliverables/"):
ref = ref[len("deliverables/"):]
ref = ref.lstrip("./")
m = re.match(r"^(.*?)(:\d+(-\d+)?)$", ref)
if m:
ref = m.group(1)
return os.path.exists(os.path.join(deliv, ref))
def check_compliance(deliv, ids, rep):
path = os.path.join(deliv, "01-requirements", "compliance.csv")
if not os.path.isfile(path):
rep.error("01-requirements/compliance.csv missing")
return
raw = open(path, encoding="utf-8-sig", errors="replace").read()
rows = list(csv.reader(io.StringIO(raw)))
if not rows:
rep.error("compliance.csv is empty")
return
header = [h.strip() for h in rows[0]]
if header != COMPLIANCE_HEADER:
rep.error(f"compliance.csv header is {header}, expected {COMPLIANCE_HEADER}")
return
seen = {}
for n, row in enumerate(rows[1:], start=2):
if not any(c.strip() for c in row):
continue
if len(row) != len(COMPLIANCE_HEADER):
rep.error(f"compliance.csv line {n}: {len(row)} fields, expected {len(COMPLIANCE_HEADER)} (quote fields with commas)")
continue
rid, status, _value, _unit, evidence, _note = [c.strip() for c in row]
if rid in seen:
rep.error(f"compliance.csv line {n}: duplicate id {rid} (first at line {seen[rid]})")
seen[rid] = n
if status not in COMPLIANCE_STATUS:
rep.error(f"compliance.csv line {n}: status '{status}' not in {sorted(COMPLIANCE_STATUS)}")
if status != "not_addressed" and not evidence:
rep.error(f"compliance.csv line {n} ({rid}): status {status} without evidence path")
for ref in split_paths(evidence):
if not evidence_exists(deliv, ref):
rep.error(f"compliance.csv line {n} ({rid}): evidence path not found: {ref}")
if ids:
missing = [i for i in ids if i not in seen]
extra = [i for i in seen if i not in ids]
if missing:
rep.error(f"compliance.csv lacks {len(missing)} requirement id(s): {', '.join(missing)}")
if extra:
rep.warn(f"compliance.csv has ids not in INPUT/requirements.md: {', '.join(extra)}")
rep.note(f"compliance.csv: {len(seen)} rows for {len(ids)} requirement ids")
else:
rep.warn("no requirement ids found in INPUT/requirements.md; row coverage not checked")
def check_ids_in_requirements(deliv, ids, rep):
folder = os.path.join(deliv, "01-requirements")
texts = ""
for p in nonempty_files(folder, (".md", ".yaml", ".yml", ".txt")):
texts += open(p, encoding="utf-8", errors="replace").read() + "\n"
if not texts:
rep.error("01-requirements/ has no requirements document (.md/.yaml) besides compliance.csv")
return
absent = [i for i in ids if i not in texts]
if absent:
rep.warn(f"requirement ids not mentioned in the 01-requirements documents: {', '.join(absent)}")
def num_or_null(x):
return x is None or isinstance(x, (int, float)) and not isinstance(x, bool)
def check_manifest(deliv, tier, rep):
path = os.path.join(deliv, "manifest.json")
if not os.path.isfile(path):
rep.error("manifest.json missing")
return
try:
man = json.load(open(path, encoding="utf-8"))
except Exception as exc: # noqa: BLE001
rep.error(f"manifest.json does not parse: {exc}")
return
if man.get("tier") != tier:
rep.error(f"manifest.json tier is {man.get('tier')!r}, expected {tier!r}")
files = man.get("files")
if not isinstance(files, list) or not files:
rep.error("manifest.json files[] missing or empty")
else:
listed = set()
for f in files:
if not isinstance(f, dict) or not {"path", "kind", "desc"} <= set(f):
rep.error(f"manifest.json files[] entry lacks path/kind/desc: {f}")
continue
listed.add(f["path"].lstrip("./"))
if not evidence_exists(deliv, f["path"]):
rep.error(f"manifest.json lists a file that does not exist: {f['path']}")
actual = set()
for p in nonempty_files(deliv):
rel = os.path.relpath(p, deliv)
if rel != "manifest.json":
actual.add(rel)
unlisted = sorted(a for a in actual if a not in listed and not any(a.startswith(l.rstrip("/") + "/") for l in listed))
if unlisted:
rep.warn(f"{len(unlisted)} deliverable file(s) not listed in manifest.json, e.g. {unlisted[:5]}")
kf = man.get("key_figures")
if not isinstance(kf, dict):
rep.error("manifest.json key_figures missing")
else:
for key in ("dimensions_mm", "mass_kg", "power_w", "unit_cost", "retail_price"):
if key not in kf:
rep.error(f"manifest.json key_figures.{key} missing (use null when unknown)")
dims = kf.get("dimensions_mm")
if dims is not None and not (isinstance(dims, list) and len(dims) == 3 and all(num_or_null(d) for d in dims)):
rep.error("key_figures.dimensions_mm must be [L, W, H] numbers or null")
for key in ("mass_kg", "power_w"):
if key in kf and not num_or_null(kf[key]):
rep.error(f"key_figures.{key} must be a number or null")
uc = kf.get("unit_cost")
if uc is not None:
if not isinstance(uc, dict) or not {"currency", "q1", "q100", "q1000"} <= set(uc):
rep.error("key_figures.unit_cost needs currency, q1, q100, q1000")
elif not all(num_or_null(uc[q]) for q in ("q1", "q100", "q1000")):
rep.error("key_figures.unit_cost q1/q100/q1000 must be numbers or null")
rp = kf.get("retail_price")
if rp is not None and (not isinstance(rp, dict) or not {"currency", "value"} <= set(rp) or not num_or_null(rp.get("value"))):
rep.error("key_figures.retail_price needs currency and a numeric value (or null)")
blocked = man.get("blocked")
if not isinstance(blocked, list):
rep.error("manifest.json blocked[] missing (use [] when nothing is blocked)")
else:
for b in blocked:
if not isinstance(b, dict) or not {"what", "why"} <= set(b):
rep.error(f"manifest.json blocked[] entry needs what and why: {b}")
def check_cost_json(deliv, rep, required):
path = os.path.join(deliv, "09-bom-cost", "cost.json")
if not os.path.isfile(path):
if required:
rep.error("09-bom-cost/cost.json missing")
return None
try:
cost = json.load(open(path, encoding="utf-8"))
except Exception as exc: # noqa: BLE001
rep.error(f"cost.json does not parse: {exc}")
return None
for key in ("currency", "bom", "unit_cost"):
if key not in cost:
rep.error(f"cost.json lacks {key}")
for key in ("bom", "unit_cost"):
block = cost.get(key)
if isinstance(block, dict) and not all(num_or_null(block.get(q)) for q in ("q1", "q100", "q1000")):
rep.error(f"cost.json {key} needs numeric q1, q100, q1000")
other = cost.get("other", {})
if not isinstance(other, dict):
rep.error("cost.json other must be an object of {name: {q1, q100, q1000}}")
else:
try:
for q in ("q1", "q100", "q1000"):
total = float(cost["bom"][q]) + sum(float(v.get(q) or 0) for v in other.values())
unit = float(cost["unit_cost"][q])
if unit > 0 and abs(total - unit) / unit > 0.02:
rep.warn(f"cost.json unit_cost.{q}={unit} differs from bom+other={total:.2f} by >2 %")
except (KeyError, TypeError, ValueError):
pass
return cost
def as_float(text):
try:
return float(str(text).replace(",", ".").strip())
except ValueError:
return None
def check_bom(deliv, cost, rep):
path = os.path.join(deliv, "09-bom-cost", "bom.csv")
if not os.path.isfile(path):
rep.error("09-bom-cost/bom.csv missing")
return
rows = list(csv.reader(io.StringIO(open(path, encoding="utf-8-sig", errors="replace").read())))
if not rows or [h.strip() for h in rows[0]] != BOM_HEADER:
rep.error(f"bom.csv header must be exactly {','.join(BOM_HEADER)}")
return
totals = {"unit_price_1": 0.0, "unit_price_100": 0.0, "unit_price_1000": 0.0}
currencies = set()
lines = 0
for n, row in enumerate(rows[1:], start=2):
if not any(c.strip() for c in row):
continue
if len(row) != len(BOM_HEADER):
rep.error(f"bom.csv line {n}: {len(row)} fields, expected {len(BOM_HEADER)}")
continue
rec = dict(zip(BOM_HEADER, [c.strip() for c in row]))
lines += 1
qty = as_float(rec["qty"])
if qty is None:
rep.error(f"bom.csv line {n}: qty '{rec['qty']}' is not a number")
continue
if not rec["mpn"] and not rec["supplier_part"]:
rep.warn(f"bom.csv line {n}: neither mpn nor supplier_part ({rec['description'][:40]})")
if rec["url"] and not rec["url"].startswith(("http://", "https://")):
rep.warn(f"bom.csv line {n}: url is not http(s)")
currencies.add(rec["currency"])
for col in totals:
price = as_float(rec[col])
if price is None:
rep.warn(f"bom.csv line {n}: {col} '{rec[col]}' is not a number")
else:
totals[col] += qty * price
rep.note(f"bom.csv: {lines} lines; sums 1/100/1000 = {totals['unit_price_1']:.2f} / "
f"{totals['unit_price_100']:.2f} / {totals['unit_price_1000']:.2f} {','.join(sorted(currencies))}")
if len(currencies) > 1:
rep.warn(f"bom.csv mixes currencies {sorted(currencies)}; totals not comparable")
elif cost and isinstance(cost.get("bom"), dict):
for q, col in (("q1", "unit_price_1"), ("q100", "unit_price_100"), ("q1000", "unit_price_1000")):
ref = as_float(cost["bom"].get(q))
if ref and abs(ref - totals[col]) / ref > 0.02:
rep.warn(f"cost.json bom.{q}={ref} differs from the bom.csv sum {totals[col]:.2f} by >2 %")
def toolchain_python():
"""The toolchain Python the RUN CONTRACT names (ddpy), else the venv, else this interpreter."""
for cand in (shutil.which("ddpy"), "/opt/dd/venv/bin/python"):
if cand and os.path.exists(cand):
return cand
return sys.executable
def check_scripts(folder, rep, run):
scripts = [p for p in nonempty_files(folder, (".py",))]
for script in scripts:
stem = os.path.splitext(script)[0]
results = stem + ".results.json"
if run:
try:
proc = subprocess.run([toolchain_python(), os.path.basename(script)], cwd=os.path.dirname(script),
capture_output=True, text=True, timeout=300)
if proc.returncode != 0:
rep.error(f"{os.path.relpath(script)} exits {proc.returncode}: {proc.stderr.strip()[-200:]}")
except subprocess.TimeoutExpired:
rep.error(f"{os.path.relpath(script)} runs longer than 5 minutes")
if not os.path.isfile(results):
rep.warn(f"{os.path.relpath(script)} has no {os.path.basename(results)}")
continue
try:
data = json.load(open(results, encoding="utf-8"))
bad = [k for k, v in data.items() if not (isinstance(v, dict) and "value" in v and "unit" in v)]
if bad:
rep.warn(f"{os.path.relpath(results)}: entries without value/unit: {bad[:5]}")
except Exception as exc: # noqa: BLE001
rep.error(f"{os.path.relpath(results)} does not parse: {exc}")
def check_full(deliv, rep, run):
for d in FULL_DIRS:
if not nonempty_files(os.path.join(deliv, d)):
rep.error(f"{d}/ missing or empty")
cad = os.path.join(deliv, "03-cad")
asm = os.path.join(cad, "assembly.step")
if not os.path.isfile(asm):
rep.error("03-cad/assembly.step missing")
else:
if not open(asm, "rb").read(64).lstrip().startswith(b"ISO-10303-21"):
rep.error("03-cad/assembly.step is not a STEP file (no ISO-10303-21 header)")
steps = [p for p in nonempty_files(cad, (".step", ".stp")) if not p.endswith("assembly.step")]
if not steps:
rep.error("03-cad/ has no per-part STEP files")
if not nonempty_files(cad, (".py", ".fcstd")):
rep.error("03-cad/ has no parametric source (.py or .FCStd)")
if not nonempty_files(os.path.join(deliv, "04-drawings"), (".pdf", ".svg")):
rep.error("04-drawings/ has no PDF or SVG drawing")
check_scripts(os.path.join(deliv, "05-calculations"), rep, run)
ele = os.path.join(deliv, "06-electronics")
if not nonempty_files(ele, (".kicad_pro",)) or not nonempty_files(ele, (".kicad_sch",)):
rep.error("06-electronics/ lacks a KiCad project (.kicad_pro + .kicad_sch)")
names = " ".join(os.path.basename(p).lower() for p in nonempty_files(ele))
if "erc" not in names:
rep.warn("06-electronics/ has no file named like an ERC report")
if nonempty_files(ele, (".kicad_pcb",)) and "drc" not in names:
rep.warn("06-electronics/ has a board but no file named like a DRC report")
if not os.path.isfile(os.path.join(deliv, "07-firmware", "platformio.ini")):
rep.error("07-firmware/platformio.ini missing")
cost = check_cost_json(deliv, rep, required=True)
check_bom(deliv, cost, rep)
if not os.path.isfile(os.path.join(deliv, "09-bom-cost", "cost.md")):
rep.error("09-bom-cost/cost.md missing")
ren = os.path.join(deliv, "11-renders")
if not nonempty_files(ren, (".png",)):
rep.error("11-renders/ has no PNG render")
glb = os.path.join(ren, "assembly.glb")
if not os.path.isfile(glb):
rep.error("11-renders/assembly.glb missing")
elif open(glb, "rb").read(4) != b"glTF":
rep.error("11-renders/assembly.glb is not a binary glTF file")
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--tier", choices=["concept", "full"], required=True)
ap.add_argument("--run-dir", default=".", help="run directory (default: current directory)")
ap.add_argument("--run-scripts", action="store_true", help="execute calculation scripts (≤5 min each)")
ap.add_argument("--json", action="store_true", help="print a JSON summary as the last line")
args = ap.parse_args()
run_dir = os.path.abspath(args.run_dir)
deliv = os.path.join(run_dir, "deliverables")
rep = Report()
if not os.path.isdir(deliv):
rep.error("deliverables/ missing")
else:
ids = requirement_ids(os.path.join(run_dir, "INPUT", "requirements.md"))
if not os.path.isdir(os.path.join(deliv, "01-requirements")):
rep.error("01-requirements/ missing")
else:
check_compliance(deliv, ids, rep)
check_ids_in_requirements(deliv, ids, rep)
if not nonempty_files(os.path.join(deliv, "02-concept"), (".md",)):
rep.error("02-concept/ has no markdown document")
report = os.path.join(deliv, "REPORT.md")
if not os.path.isfile(report):
rep.error("REPORT.md missing")
elif os.path.getsize(report) < 1500:
rep.warn("REPORT.md is shorter than 1500 bytes")
check_manifest(deliv, args.tier, rep)
check_scripts(os.path.join(deliv, "02-concept"), rep, args.run_scripts)
if args.tier == "concept":
check_cost_json(deliv, rep, required=False)
started = [d for d in FULL_DIRS if d != "09-bom-cost" and nonempty_files(os.path.join(deliv, d))]
if started:
rep.warn(f"concept tier: detailed-design folders present ({', '.join(started)}); they are not judged at this tier")
else:
check_full(deliv, rep, args.run_scripts)
for msg in rep.notes:
print(f"NOTE {msg}")
for msg in rep.warnings:
print(f"WARN {msg}")
for msg in rep.errors:
print(f"ERROR {msg}")
print(f"dd_check: tier={args.tier} errors={len(rep.errors)} warnings={len(rep.warnings)} -> "
f"{'PASS' if not rep.errors else 'FAIL'}")
if args.json:
print(json.dumps({"tier": args.tier, "pass": not rep.errors, "errors": rep.errors,
"warnings": rep.warnings, "notes": rep.notes}))
return 0 if not rep.errors else 1
if __name__ == "__main__":
sys.exit(main())
files/method/gates/seq.py (720 wierszy)
#!/usr/bin/env python3
"""seq.py - the sequencer's mechanics for a SKELETON run (standard library only; run from the run directory).
Scoreboard and ledger:
init --tier concept|full work/SCOREBOARD.md from INPUT/requirements.md (weight from "Priority: n/5" or
"Weight: n", else 3) plus one row per deliverable folder of the tier
rank [--top 10] [--late] refresh status from compliance.csv; blocking folders, pivot checks, ranking,
and a grouping into <= 3 workers with non-overlapping scopes
quoters SCOPE what must join a brief with SCOPE: files and compliance ids that quote a home inside
it, calc scripts that read a file inside it (quotes move with their home)
merge N [--skip b,c] [--include-unfinished] claims.csv of the workers of increment N that wrote note.md ->
01-requirements/compliance.csv + scoreboard; FIDELITY/PIVOT lines -> scoreboard
(a worker folder without note.md is skipped; exit 1: rows rejected)
table [--write FILE] status table per requirement; --write replaces it between the STATUS-TABLE markers
manifest --tier T deliverables/manifest.json files[] = the tree (kind/desc kept); key_figures, blocked kept
wait N --workers a,b [--until HH:MM] [--max-seconds 240] until work/inc/N-<w>/note.md exist (exit 0),
the deadline passed (2) or max-seconds elapsed (3: call again)
Snapshots of deliverables/ (+ work/SCOREBOARD.md) in work/snapshots/ID/, unchanged files hard-linked:
snap ID [--force] | changed ID [--scope S ...] (exit 1 if a change lies outside every scope)
revert ID (--paths P ... | --outside S ... | --all) [--ledger] | disjoint S1 S2 ... | prune [--keep 4] | snaps
A snapshot is GREEN when the last sequencer check_all was GREEN on exactly this package (work/last-check.json)
and SKELETON while a folder of the tier is empty. revert refuses --paths/--all to a SKELETON snapshot and --all
to one that is not GREEN: a skeleton is repaired forward, never reverted.
A scope is ";"-separated paths relative to deliverables/ ("02-concept/calc/" = folder, "02-concept/calc/power.*" =
every file of that stem). Ledger files are never inside a worker scope: REPORT.md, manifest.json,
01-requirements/compliance.csv, 02-concept/increments.md.
Score: requirement = weight x risk (H3 M2 L1; not_met counts H) x gap (none 4, estimate 3, calculation 2,
simulation/sourced 1) / effort (S1 M2 L3); folder below target = 3 x gap (fidelity 0:4, 1:3, 2:2), x2 with
--late when fidelity < 2. A scoreboard note starting with ACCEPTED removes the requirement from the ranking.
LOW = no folder below fidelity 2 and no requirement scoring 6 or more.
"""
import argparse
import csv
import glob
import hashlib
import io
import json
import os
import re
import shutil
import sys
import time
sys.dont_write_bytecode = True
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import dd_check as ddc # noqa: E402 the shared RUN CONTRACT checker: one definition of ids and evidence paths
HEAD = ddc.COMPLIANCE_HEADER
STATUSES = ["met", "partly", "not_met", "not_addressed"]
PESSIMISM = {"not_met": 0, "partly": 1, "met": 2, "not_addressed": 3}
GAP = {"none": 4, "estimate": 3, "calculation": 2, "simulation": 1, "sourced": 1}
RISK, EFFORT, FGAP = {"H": 3, "M": 2, "L": 1}, {"S": 1, "M": 2, "L": 3}, {0: 4, 1: 3, 2: 2}
FOLDERS = ["01-requirements", "02-concept"] + ddc.FULL_DIRS
LEDGER = ("REPORT.md", "manifest.json", "01-requirements/compliance.csv", "02-concept/increments.md")
JUNK = {".pio", "__pycache__", ".git"}
REQ_COLS = ["req_id", "title", "weight", "status", "value", "quality", "risk", "effort", "scope", "note"]
DIR_COLS = ["folder", "fidelity", "target", "note"]
HEADING = re.compile(r"^#{1,6}\s+([A-Z][A-Z0-9]{1,7}-[A-Z0-9]{1,7}-\d{1,4})\b(.*)$")
WEIGHT = re.compile(r"(?i)\b(?:priority|weight|importance)\b[^0-9\n]{0,15}(\d+(?:\.\d+)?)(?:\s*/\s*(\d+))?")
BEGIN, END = "<!-- STATUS-TABLE:BEGIN -->", "<!-- STATUS-TABLE:END -->"
TAG = re.compile(r"\[=([^\]@\n]+)@([^\]\s]+?\.(?:json|csv))\]") # a quote tag; check_all.py uses this one too
ROOT = DELIV = BOARD = COMP = SNAPS = REQS = ""
def norm(p):
p = (p or "").strip().strip("`'\"").replace("\\", "/")
while p.startswith("./"):
p = p[2:]
return p[13:] if p.startswith("deliverables/") else p
def scopes_of(args):
return [norm(c) for a in (args or []) for c in a.replace(",", ";").split(";") if c.strip()]
def under(rel, scope):
s = scope.rstrip("/")
if s.endswith(".*"): # stem scope: "02-concept/calc/power.*" = power.py, power.results.json, power.png ...
return rel.startswith(s[:-1]) and "/" not in rel[len(s) - 1:]
return s == "" or rel == s or rel.startswith(s + "/")
def stem_scope(ref):
"""Evidence path -> board scope: a calculation becomes its stem ("02-concept/calc/power.*"), else the file."""
ref = re.sub(r":\d+(-\d+)?$", "", ref)
if ref.startswith(("02-concept/calc/", "05-calculations/")) and not ref.endswith("/"):
folder, name = os.path.split(ref)
return f"{folder}/{name[:-13] if name.endswith('.results.json') else os.path.splitext(name)[0]}.*"
return ref
def in_scope(rel, scopes):
return rel not in LEDGER and any(under(rel, s) for s in scopes)
def num(x, default):
try:
return float(str(x).strip())
except ValueError:
return default
def write(path, text):
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path + ".tmp", "w", encoding="utf-8", newline="") as fh:
fh.write(text)
os.replace(path + ".tmp", path)
def md_table(cols, rows):
cell = lambda x: str(x or "").replace("|", "/").replace("\n", " ").strip() # noqa: E731
return ["| " + " | ".join(cols) + " |", "|" + "---|" * len(cols)] + \
["| " + " | ".join(cell(r.get(c)) for c in cols) + " |" for r in rows]
# ------------------------------------------------------------------ scoreboard
class Board:
def __init__(self):
self.meta, self.reqs, self.dirs = {"tier": "concept", "pivot": "unused"}, [], []
def load(self):
if not os.path.isfile(BOARD):
sys.exit("work/SCOREBOARD.md missing: run `python3 method/gates/seq.py init --tier <tier>` first")
section = header = None
for line in open(BOARD, encoding="utf-8").read().splitlines():
s = line.strip()
m = re.match(r"^(tier|pivot):\s*(.*)$", s)
if s.startswith("## "):
section, header = s[3:].strip().lower(), None
elif m and section is None:
self.meta[m.group(1)] = m.group(2).strip()
elif s.startswith("|") and section in ("requirements", "folders"):
cells = [c.strip() for c in s.strip("|").split("|")]
if header is None:
header = [c.lower() for c in cells]
elif not all(set(c) <= set("-: ") for c in cells):
(self.reqs if section == "requirements" else self.dirs).append(dict(zip(header, cells)))
return self
def row(self, rid):
for r in self.reqs:
if r.get("req_id") == rid:
return r
self.reqs.append({"req_id": rid, "weight": "3", "status": "not_addressed", "quality": "none", "risk": "M",
"effort": "M", "note": "added by seq.py"})
return self.reqs[-1]
def save(self):
text = ["# SCOREBOARD — sequencer-owned; workers read it, never write it", "",
f"tier: {self.meta.get('tier')}", f"pivot: {self.meta.get('pivot')}", "",
f"Updated {time.strftime('%Y-%m-%d %H:%M')}. Scoring: see the header of method/gates/seq.py. Edit "
"by hand only the cells weight (to correct it from INPUT/requirements.md), quality, risk, effort, "
"scope, note, fidelity, target and the pivot line; "
"seq.py fills status and value from compliance.csv. A note starting with ACCEPTED removes a "
"requirement from the ranking.", "", "## Requirements", ""]
text += md_table(REQ_COLS, self.reqs) + ["", "## Folders", ""] + md_table(DIR_COLS, self.dirs) + [""]
write(BOARD, "\n".join(text))
def load_comp(ids):
rows = {}
if os.path.isfile(COMP):
for row in list(csv.reader(open(COMP, encoding="utf-8-sig", newline="")))[1:]:
if any(c.strip() for c in row):
row = ([c.strip() for c in row] + [""] * 6)[:6]
rows[row[0]] = dict(zip(HEAD, row))
for rid in ids:
rows.setdefault(rid, dict(zip(HEAD, [rid, "not_addressed", "", "", "", "not yet addressed"])))
return rows
def save_comp(ids, rows):
extra = [k for k in rows if k not in ids]
if extra:
print(f"DROPPED from compliance.csv (not in INPUT/requirements.md): {', '.join(extra)}")
buf = io.StringIO()
w = csv.writer(buf, lineterminator="\n")
w.writerow(HEAD)
w.writerows([rows[rid].get(c, "") for c in HEAD] for rid in ids)
write(COMP, buf.getvalue())
def sync(board, ids, comp):
for rid in ids:
r = board.row(rid)
r["status"], r["value"] = comp[rid]["status"], (comp[rid]["value"] + " " + comp[rid]["unit"]).strip()
def cmd_init(a):
if os.path.exists(BOARD) and not a.force:
sys.exit("work/SCOREBOARD.md exists (use --force to overwrite)")
ids = ddc.requirement_ids(REQS)
if not ids:
sys.exit("no requirement ids found in INPUT/requirements.md")
info = {i: {"title": "", "weight": 3, "found": False} for i in ids}
body, cur, level = {}, None, 0
for line in open(REQS, encoding="utf-8", errors="replace").read().splitlines():
m, depth = HEADING.match(line.strip()), len(line.strip()) - len(line.strip().lstrip("#"))
if m:
cur, level = (m.group(1) if m.group(1) in info else None), depth
if cur and not info[cur]["title"]:
info[cur]["title"] = m.group(2).strip().lstrip("—–-:· ").strip(" *_")[:48]
elif depth and depth <= level: # a heading at the same or a higher level ends the requirement
cur = None
elif cur:
body.setdefault(cur, []).append(line)
for rid, lines in body.items():
m = WEIGHT.search("\n".join(lines))
if m:
n, d = float(m.group(1)), float(m.group(2) or 5)
info[rid].update(weight=max(1, min(5, round(n * 5 / d))) if d else 3, found=True)
b = Board()
b.meta = {"tier": a.tier, "pivot": "unused"}
b.reqs = [{"req_id": i, "title": v["title"], "weight": str(v["weight"]), "status": "not_addressed",
"quality": "none", "risk": "M", "effort": "M", "note": "" if v["found"] else "weight ASSUMPTION 3"}
for i, v in info.items()]
b.dirs = [{"folder": f, "fidelity": "0", "target": "3"} for f in (FOLDERS[:2] if a.tier == "concept" else FOLDERS)]
b.save()
hist = {}
for v in info.values():
hist[v["weight"]] = hist.get(v["weight"], 0) + 1
miss = sum(not v["found"] for v in info.values())
print(f"SCOREBOARD: {len(ids)} ids, weights {dict(sorted(hist.items(), reverse=True))}"
f"{f', {miss} without a stated weight (3 assumed: correct their weight cells by hand)' if miss else ''}; "
f"folders: {len(b.dirs)}")
def cmd_rank(a):
b = Board().load()
ids = ddc.requirement_ids(REQS)
if os.path.isfile(COMP):
sync(b, ids, load_comp(ids))
b.save()
pivot = b.meta.get("pivot", "unused")
fid = {d["folder"]: int(num(d.get("fidelity"), 0)) for d in b.dirs}
blocking = [f for f, v in fid.items() if v == 0]
print(f"RANK tier={b.meta.get('tier')} pivot={pivot} at {time.strftime('%H:%M')}")
if blocking:
print(f"BLOCKING: fidelity 0 in {', '.join(blocking)} — fill these before any deeper work")
cands = []
for r in b.reqs:
st, q = r.get("status", ""), r.get("quality", "").lower()
if r.get("note", "").upper().startswith("ACCEPTED"):
continue
w = num(r.get("weight"), 3)
if st == "not_met" and w >= 4 and q in ("calculation", "simulation", "sourced"):
print(f"{'PIVOT CHECK' if pivot.startswith('unused') else 'MISS (pivot used)'}: {r['req_id']} not_met "
f"with {q} evidence, weight {w:g} — method/POLICY.md §5")
if (st in ("met", "partly") and q == "none") or (st == "not_addressed" and q not in ("", "none")):
print(f"INCONSISTENT: {r['req_id']} status {st} with evidence quality {q or 'blank'}")
risk = 3 if st == "not_met" else max(RISK.get(r.get("risk", "").upper(), 2), 2 if st == "partly" else 1)
scope = norm(r.get("scope")) or "?"
cands.append(dict(kind="req", key=r["req_id"], score=w * risk * GAP.get(q, 4) / EFFORT.get(
r.get("effort", "").upper(), 2), fid=fid.get(scope.split("/")[0], 9), w=w, scope=scope,
info=f"w{w:g} {st} q={q} risk={r.get('risk')} effort={r.get('effort')} {r.get('title', '')[:28]}"))
for d in b.dirs:
fi, tg = int(num(d.get("fidelity"), 0)), int(num(d.get("target"), 3))
if fi < tg:
cands.append(dict(kind="folder", key=d["folder"], score=3 * FGAP.get(fi, 0) * (2 if a.late and fi < 2 else 1),
fid=fi, w=3, scope=d["folder"] + "/", info=f"fidelity {fi} -> {tg}"))
cands.sort(key=lambda c: (not (c["kind"] == "folder" and c["key"] in blocking), -c["score"], c["fid"], -c["w"]))
for n, c in enumerate(cands[:a.top], 1):
print(f"{n:>2} {c['score']:>5.1f} {c['kind']:6} {c['key']:16} {c['scope'][:26]:26} {c['info']}")
groups = []
for c in cands[:max(a.top, 6)]:
if blocking and not (c["kind"] == "folder" and c["key"] in blocking):
break
hit = [g for g in groups if g["scope"] == c["scope"] or c["scope"] != "?" != g["scope"] and (
under(c["scope"].rstrip("/"), g["scope"]) or under(g["scope"].rstrip("/"), c["scope"]))]
if hit and len(hit[0]["items"]) < 3:
hit[0]["items"].append(c)
elif not hit and len(groups) < 3:
groups.append({"scope": c["scope"], "items": [c]})
print("SUGGESTION (widen each scope to the files its goal needs; keep scopes disjoint):")
for letter, g in zip("abc", groups):
print(f" {letter}: {g['scope']} <- " + ", ".join(f"{c['key']} ({c['score']:.1f})" for c in g["items"]))
best = max((c["score"] for c in cands if c["kind"] == "req"), default=0)
if not blocking and best < 6 and all(v >= 2 for v in fid.values()):
print("LOW: every folder at fidelity >= 2 and no requirement scores 6 or more — method/POLICY.md §6 stop rules")
def cmd_quoters(a):
sc, cur = scopes_of(a.scope), walk()
inside, add = {r for r in cur if any(under(r, s) for s in sc)}, []
for rel in sorted(cur): # a script outside the scope that reads a file inside it: its results move too
if rel.endswith(".py") and rel not in inside and rel.startswith(("02-concept/", "05-calculations/")):
text = open(cur[rel], encoding="utf-8", errors="replace").read()
reads = sorted({os.path.basename(h) for h in inside if not h.endswith(".py") and os.path.basename(h) in text})
if reads:
add.append(rel[:-3] + ".*")
inside |= {r for r in cur if under(r, add[-1])}
print(f"SCRIPT {rel} reads {', '.join(reads)}")
quoting, ids = {}, set()
for rel in sorted(cur):
if rel.endswith(".md") and rel not in inside and rel != "02-concept/increments.md":
tags = {f"{m.group(1)}@{norm(m.group(2))}" for m in
TAG.finditer(open(cur[rel], encoding="utf-8", errors="replace").read()) if norm(m.group(2)) in inside}
if tags:
quoting[rel] = sorted(tags)
if os.path.isfile(COMP):
for row in list(csv.reader(open(COMP, encoding="utf-8-sig", newline="")))[1:]:
if len(row) > 5 and any(norm(m.group(2)) in inside for m in TAG.finditer(row[5])):
ids.add(row[0].strip())
for rel, tags in quoting.items():
print(f"QUOTES {rel}{' (ledger: you update it in step 7)' if rel in LEDGER else ''}: {', '.join(tags[:4])}"
f"{' ...' if len(tags) > 4 else ''}")
print("ADD TO SCOPE: " + (";".join(add + [r for r in quoting if r not in LEDGER]) or "nothing"))
print("IDS IN FOCUS (compliance value quotes this scope; the worker re-claims them): " + (", ".join(sorted(ids)) or "none"))
def cmd_merge(a):
ids, b = ddc.requirement_ids(REQS), Board().load()
comp, skip = load_comp(ids), {x.strip() for x in a.skip.split(",") if x.strip()}
used, rejects, claims = [], [], {}
for d in sorted(glob.glob(os.path.join(ROOT, "work", "inc", f"{a.inc}-*"))):
wk = os.path.basename(d)
if not os.path.isdir(d) or wk[len(a.inc) + 1:] in skip:
continue
if not os.path.isfile(os.path.join(d, "note.md")) and not a.include_unfinished:
print(f"SKIPPED {wk}: no note.md (not finished, stopped or failed; its claims are not merged — MAIN.md §9)")
continue
used.append(wk)
path = os.path.join(d, "claims.csv")
rows = list(csv.reader(open(path, encoding="utf-8-sig", newline=""))) if os.path.isfile(path) else []
if not rows:
print(f"NO CLAIMS from {wk}")
continue
hdr = [h.strip().lower() for h in rows[0]]
if hdr[:6] != HEAD:
rejects.append(f"{wk}/claims.csv: header must start with {','.join(HEAD)}")
continue
for n, row in enumerate(rows[1:], 2):
if not any(c.strip() for c in row):
continue
if not len(HEAD) <= len(row) <= len(hdr):
rejects.append(f"{wk}/claims.csv line {n}: {len(row)} fields, expected {len(hdr)} (quote fields with commas)")
continue
rec = dict(zip(hdr, [c.strip() for c in row] + [""] * (len(hdr) - len(row))))
rec.update(quality=rec.get("quality", "").lower(), risk=rec.get("risk", "").upper(),
effort=rec.get("effort", "").upper())
bad = [f"evidence not found: {p}" for p in ddc.split_paths(rec["evidence"])
if not ddc.evidence_exists(DELIV, p)]
if rec["req_id"] not in ids:
bad.insert(0, "id not in INPUT/requirements.md")
if rec["status"] not in STATUSES:
bad.insert(0, f"status '{rec['status']}' not in {STATUSES}")
elif rec["status"] != "not_addressed" and not rec["evidence"]:
bad.insert(0, "needs an evidence path")
for col, ok in (("quality", GAP), ("risk", RISK), ("effort", EFFORT)):
if rec[col] and rec[col] not in ok:
bad.append(f"{col} '{rec[col]}' not in {sorted(ok)}")
if bad:
rejects.append(f"{wk}/claims.csv line {n} ({rec['req_id']}): {'; '.join(bad)}")
else:
claims.setdefault(rec["req_id"], []).append((wk, rec))
changes = []
for rid in ids:
if rid not in claims:
continue
pool = [x for x in claims[rid] if x[1]["status"] != "not_addressed"] or claims[rid]
wk, rec = min(pool, key=lambda x: PESSIMISM[x[1]["status"]])
if len({x[1]["status"] for x in pool}) > 1:
print(f"CONFLICT {rid}: " + ", ".join(f"{w}={r['status']}" for w, r in pool) + f" -> kept {rec['status']}")
br, old = b.row(rid), comp[rid]
if rec["status"] == "not_addressed" and old["status"] != "not_addressed":
print(f"IGNORED {rid}: not_addressed from {wk} does not replace {old['status']}")
continue
br.update({c: rec[c] for c in ("quality", "risk", "effort") if rec.get(c)})
new = {c: rec[c] for c in HEAD}
others = sorted({w for w, _r in claims[rid]} - {wk})
new["note"] = (new["note"] + (f" (also claimed by {', '.join(others)})" if others else "")).strip()
if (old["status"], old["value"], old["unit"]) != (new["status"], new["value"], new["unit"]):
changes.append(f" {rid}: {old['status']} {old['value']}{old['unit']} -> {new['status']} "
f"{new['value']} {new['unit']} [{wk}]")
comp[rid] = new
if not br.get("scope"):
br["scope"] = stem_scope(norm((ddc.split_paths(rec["evidence"]) or [""])[0].split("#")[0]))
dirs, fids = {d["folder"]: d for d in b.dirs}, {}
for wk in used:
note = os.path.join(ROOT, "work", "inc", wk, "note.md")
if not os.path.isfile(note):
print(f"NO NOTE from {wk}: not finished or failed (MAIN.md §9)")
continue
for line in open(note, encoding="utf-8", errors="replace"):
m = re.match(r"^\s*FIDELITY\s+(\S+)\s+([0-3])\b", line)
if m and norm(m.group(1)).rstrip("/") in dirs:
fids.setdefault(norm(m.group(1)).rstrip("/"), []).append((int(m.group(2)), wk))
if re.match(r"^\s*PIVOT\s+yes\b", line, re.I):
print(f"PIVOT REQUEST [{wk}]: {line.strip()[9:].strip()} — method/POLICY.md §5")
for folder, vals in fids.items(): # several workers on one folder: the lowest report counts
low = str(min(vals)[0])
if dirs[folder]["fidelity"] != low:
print(f"FIDELITY {folder}: {dirs[folder]['fidelity']} -> {low} [{', '.join(f'{w}={v}' for v, w in vals)}]")
dirs[folder]["fidelity"] = low
save_comp(ids, comp)
sync(b, ids, comp)
b.save()
print(f"MERGED {a.inc}: {', '.join(used) or 'no workers'}; {sum(map(len, claims.values()))} rows accepted, "
f"{len(rejects)} rejected\n" + "\n".join(changes))
print("STATUS " + " · ".join(f"{s} {sum(comp[i]['status'] == s for i in ids)}" for s in STATUSES))
left = [i for i in ids if comp[i]["status"] == "not_addressed"]
if left:
print(f"NOT ADDRESSED ({len(left)}): {', '.join(left[:15])}{' ...' if len(left) > 15 else ''}")
for r in rejects:
print("REJECTED " + r)
return 1 if rejects else 0
def cmd_table(a):
ids = ddc.requirement_ids(REQS)
comp = load_comp(ids)
rows_b = {r.get("req_id"): r for r in (Board().load().reqs if os.path.isfile(BOARD) else [])}
cols = ["Requirement", "Title", "Weight", "Status", "Value", "Evidence quality", "Risk", "Evidence"]
rows = [dict(zip(cols, [i, rows_b.get(i, {}).get("title"), rows_b.get(i, {}).get("weight"), comp[i]["status"],
(comp[i]["value"] + " " + comp[i]["unit"]).strip(), rows_b.get(i, {}).get("quality"),
rows_b.get(i, {}).get("risk"),
"; ".join(f"`{p}`" for p in ddc.split_paths(comp[i]["evidence"]))])) for i in ids]
counts = " · ".join(f"{s} {sum(r['Status'] == s for r in rows)}" for s in STATUSES)
text = "\n".join([BEGIN, f"All {len(ids)} requirement ids (statuses are predictions where the evidence is an "
f"estimate or calculation): {counts}.", ""] + md_table(cols, rows) + [END])
if not a.write:
print(text)
return
path = a.write if os.path.isabs(a.write) else os.path.join(ROOT, a.write)
old = open(path, encoding="utf-8").read() if os.path.isfile(path) else ""
if BEGIN in old and END in old:
new = old[:old.index(BEGIN)] + text + old[old.index(END) + len(END):]
else:
new = old.rstrip() + "\n\n## Status per requirement\n\n" + text + "\n"
print("NOTE: no STATUS-TABLE markers, so the table was appended at the end: move the section with its markers "
"to its place in the handbook §15 order")
write(path, new)
print(f"status table -> {a.write}: {counts}")
def cmd_manifest(a):
path = os.path.join(DELIV, "manifest.json")
try:
man = json.load(open(path, encoding="utf-8")) if os.path.isfile(path) else {}
except ValueError as exc:
print(f"manifest.json did not parse ({exc}); rebuilt")
man = {}
old = {norm(f["path"]): f for f in man.get("files", []) if isinstance(f, dict) and f.get("path")}
files = []
for rel, full in sorted(walk().items()):
if rel == "manifest.json" or rel.endswith((".pyc", ".tmp", "~")) or "-backups/" in rel or not os.path.getsize(full):
continue
prev, desc = old.get(rel, {}), os.path.basename(rel)
if rel.endswith(".md"):
desc = next((ln.lstrip("#").strip() for ln in open(full, encoding="utf-8", errors="replace")
if ln.startswith("#")), desc)[:100]
elif rel.endswith(".results.json"):
desc = f"results of {os.path.basename(rel)[:-13]}.py"
files.append({"path": rel, "kind": prev.get("kind") or ("report" if rel == "REPORT.md" else (
rel.split("/")[0][3:] if rel.split("/")[0] in FOLDERS else "other")), "desc": prev.get("desc") or desc})
kf = man.get("key_figures") if isinstance(man.get("key_figures"), dict) else {}
for k in ("dimensions_mm", "mass_kg", "power_w", "unit_cost", "retail_price"):
kf.setdefault(k, None)
new = {**man, "tier": a.tier, "files": files, "key_figures": kf,
"blocked": man.get("blocked") if isinstance(man.get("blocked"), list) else []}
write(path, json.dumps(new, indent=2, ensure_ascii=False) + "\n")
print(f"manifest: {len(files)} files (+{len(set(f['path'] for f in files) - set(old))} "
f"-{len(set(old) - set(f['path'] for f in files))}); key_figures null: "
f"{', '.join(k for k, v in kf.items() if v is None) or 'none'}; blocked {len(new['blocked'])}")
def cmd_wait(a):
notes = {w: os.path.join(ROOT, "work", "inc", f"{a.inc}-{w}", "note.md") for w in a.workers.split(",") if w}
deadline = None
if a.until:
hh, mm = (int(x) for x in a.until.split(":")[:2])
t = time.localtime()
deadline = time.mktime((t.tm_year, t.tm_mon, t.tm_mday, hh, mm, 0, 0, 0, -1))
deadline += 86400 if deadline < time.time() - 43200 else 0
t0 = time.time()
while True:
missing = [f"{a.inc}-{w}" for w, p in notes.items() if not os.path.isfile(p)]
if not missing:
print(f"ALL RETURNED ({', '.join(notes)}) at {time.strftime('%H:%M:%S')}")
return 0
if deadline is not None and time.time() >= deadline:
print(f"DEADLINE {a.until} passed at {time.strftime('%H:%M:%S')}; no note.md from {', '.join(missing)}")
return 2
if time.time() - t0 >= a.max_seconds:
print(f"STILL WAITING at {time.strftime('%H:%M:%S')} for {', '.join(missing)} — call wait again")
return 3
time.sleep(10)
# ------------------------------------------------------------------ snapshots
def walk(base=None):
out, base = {}, base or DELIV
for root, dirs, names in os.walk(base):
dirs[:] = [d for d in dirs if d not in JUNK]
for n in names:
full = os.path.join(root, n)
if os.path.isfile(full):
out[os.path.relpath(full, base).replace(os.sep, "/")] = full
return out
def tree_digest(hashes):
"""One hash of a package state {rel: sha256}; check_all.py stores it with its GREEN/RED result."""
return hashlib.sha256("".join(f"{r}\0{h}\n" for r, h in sorted(hashes.items())).encode()).hexdigest()
def sha(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for block in iter(lambda: fh.read(1 << 20), b""):
h.update(block)
return h.hexdigest()
def snaps():
found = [(os.path.getmtime(p), os.path.basename(os.path.dirname(p)))
for p in glob.glob(os.path.join(SNAPS, "*", "index.json"))]
return [s for _t, s in sorted(found)]
def snapinfo(sid):
p = os.path.join(SNAPS, sid, "index.json")
if not os.path.isfile(p):
sys.exit(f"snapshot {sid} not found; take it first (seq.py snap {sid})")
return json.load(open(p, encoding="utf-8"))
def index(sid):
return snapinfo(sid)["files"]
def cmd_snap(a):
dest = os.path.join(SNAPS, a.id)
if os.path.exists(dest):
if not a.force:
sys.exit(f"snapshot {a.id} exists (use another id or --force)")
shutil.rmtree(dest)
os.makedirs(dest)
prev = [s for s in snaps() if s != a.id]
pidx = index(prev[-1]) if prev else {}
files, new = {}, 0
for rel, full in sorted(walk().items()):
digest, target = sha(full), os.path.join(dest, "deliverables", rel)
os.makedirs(os.path.dirname(target), exist_ok=True)
try:
if pidx.get(rel, {}).get("sha256") != digest:
raise OSError
os.link(os.path.join(SNAPS, prev[-1], "deliverables", rel), target)
except OSError:
shutil.copy2(full, target)
new += os.path.getsize(full)
files[rel] = {"sha256": digest, "size": os.path.getsize(full)}
try:
mark = json.load(open(os.path.join(ROOT, "work", "last-check.json"), encoding="utf-8"))
except (OSError, ValueError):
mark = {}
green = isinstance(mark, dict) and mark.get("status") == "GREEN" and \
mark.get("digest") == tree_digest({r: f["sha256"] for r, f in files.items()})
tier = Board().load().meta.get("tier") if os.path.isfile(BOARD) else "concept"
empty = [f for f in (FOLDERS[:2] if tier == "concept" else FOLDERS) if not any(r.startswith(f + "/") for r in files)]
if os.path.isfile(BOARD):
shutil.copy2(BOARD, os.path.join(dest, "SCOREBOARD.md"))
write(os.path.join(dest, "index.json"), json.dumps({"time": time.strftime("%H:%M:%S"), "green": green,
"skeleton": empty, "files": files}))
print(f"snapshot {a.id}: {len(files)} files, {new / 1e6:.2f} MB copied (rest hard-linked); "
+ ("GREEN" if green else "NOT GREEN (no GREEN check_all on exactly this package)")
+ (f"; SKELETON (empty: {', '.join(empty)}): repair forward, never revert" if empty else ""))
def changes(sid):
idx, cur = index(sid), walk()
out = []
for rel in sorted(set(idx) | set(cur)):
if rel not in cur:
out.append(("D", rel))
elif rel not in idx:
out.append(("A", rel))
elif os.path.getsize(cur[rel]) != idx[rel]["size"] or sha(cur[rel]) != idx[rel]["sha256"]:
out.append(("M", rel))
return out
def cmd_changed(a):
sc, bad, rows = scopes_of(a.scope), 0, changes(a.id)
for kind, rel in rows:
mark = ""
if a.scope is not None and not in_scope(rel, sc):
mark, bad = (" <- LEDGER file changed during the increment" if rel in LEDGER else " <- OUTSIDE SCOPE"), bad + 1
print(f"{kind} {rel}{mark}")
print(f"changed since {a.id}: {len(rows)} file(s)" + (f", {bad} outside the scopes" if a.scope is not None else ""))
return 1 if bad else 0
def cmd_revert(a):
info, cur = snapinfo(a.id), walk()
idx = info["files"]
if (a.all or a.paths) and info.get("skeleton", True):
sys.exit(f"REFUSED: increment {a.id} builds the skeleton ({', '.join(info.get('skeleton') or ['?'])} empty at "
f"snapshot {a.id}): repair it forward, never revert it (MAIN.md §9)")
if a.all and not info.get("green"):
sys.exit(f"REFUSED: snapshot {a.id} is not a GREEN package: revert --all only to a GREEN snapshot "
"(seq.py snaps), else repair forward (MAIN.md §9)")
if a.all:
targets = set(idx) | set(cur)
elif a.paths:
targets = {r for r in set(idx) | set(cur) if any(under(r, p) for p in scopes_of(a.paths))}
else:
targets = {r for _k, r in changes(a.id) if not in_scope(r, scopes_of(a.outside))}
if a.ledger:
targets |= set(LEDGER) & (set(idx) | set(cur))
done = []
for rel in sorted(targets):
dst = os.path.join(DELIV, rel)
if rel in idx:
if os.path.isfile(dst) and sha(dst) == idx[rel]["sha256"]:
continue
os.makedirs(os.path.dirname(dst), exist_ok=True)
if os.path.lexists(dst):
os.remove(dst)
shutil.copy2(os.path.join(SNAPS, a.id, "deliverables", rel), dst)
done.append(f"restored {rel}")
elif os.path.lexists(dst):
os.remove(dst)
done.append(f"deleted {rel} (absent at {a.id})")
saved = os.path.join(SNAPS, a.id, "SCOREBOARD.md")
if (a.all or a.ledger) and os.path.isfile(saved):
shutil.copy2(saved, BOARD)
done.append("restored work/SCOREBOARD.md")
for root, _dirs, _files in sorted(os.walk(DELIV), key=lambda x: -len(x[0])):
if root != DELIV and not os.listdir(root):
os.rmdir(root)
print("\n".join(done + [f"revert to {a.id}: {len(done)} change(s) undone"]))
def cmd_disjoint(a):
sc, bad = [scopes_of([s]) for s in a.scopes], 0
for i, s in enumerate(sc):
for p in s:
if p.rstrip("/") in LEDGER:
print(f"scope {i + 1} names ledger file {p}")
bad += 1
for j in range(i + 1, len(sc)):
for q in sc[j]:
if under(p.rstrip("/"), q) or under(q.rstrip("/"), p):
print(f"OVERLAP scope {i + 1} '{p}' / scope {j + 1} '{q}'")
bad += 1
print(f"disjoint: {'NO' if bad else 'yes'} ({len(sc)} scopes)")
return 1 if bad else 0
def du(path):
seen, total = set(), 0
for root, _d, names in os.walk(path):
for n in names:
st = os.lstat(os.path.join(root, n))
if (st.st_dev, st.st_ino) not in seen:
seen.add((st.st_dev, st.st_ino))
total += st.st_size
return total
def cmd_prune(a):
all_ = snaps()
keep = set(all_[-a.keep:] if a.keep else []) | {"0"}
for sid in all_:
if sid not in keep:
shutil.rmtree(os.path.join(SNAPS, sid))
print(f"pruned {[s for s in all_ if s not in keep]}; kept {[s for s in all_ if s in keep]}; "
f"{du(SNAPS) / 1e6:.1f} MB")
def cmd_snaps(_a):
for sid in snaps():
info = snapinfo(sid)
print(f"{sid:>6} {info['time']} {len(info['files'])} files {'GREEN' if info.get('green') else 'not green'}"
f"{' SKELETON' if info.get('skeleton', True) else ''}")
print(f"snapshots use {du(SNAPS) / 1e6:.1f} MB" if os.path.isdir(SNAPS) else "no snapshots")
def main():
global ROOT, DELIV, BOARD, COMP, SNAPS, REQS
ap = argparse.ArgumentParser(description="SKELETON sequencer mechanics (see the module docstring).")
ap.add_argument("--run-dir", default=".")
sub = ap.add_subparsers(dest="cmd", required=True)
for name, spec in {
"init": [("--tier", dict(choices=["concept", "full"], required=True)), ("--force", dict(action="store_true"))],
"rank": [("--top", dict(type=int, default=10)), ("--late", dict(action="store_true"))],
"quoters": [("scope", dict(nargs="+"))],
"merge": [("inc", {}), ("--skip", dict(default="")), ("--include-unfinished", dict(action="store_true"))],
"table": [("--write", {})],
"manifest": [("--tier", dict(choices=["concept", "full"], required=True))],
"wait": [("inc", {}), ("--workers", dict(required=True)), ("--until", {}),
("--max-seconds", dict(type=int, default=240))],
"snap": [("id", {}), ("--force", dict(action="store_true"))],
"changed": [("id", {}), ("--scope", dict(nargs="*"))],
"disjoint": [("scopes", dict(nargs="+"))],
"prune": [("--keep", dict(type=int, default=4))],
"snaps": [],
}.items():
p = sub.add_parser(name)
for flag, kw in spec:
p.add_argument(flag, **kw)
p = sub.add_parser("revert")
p.add_argument("id")
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--paths", nargs="+")
g.add_argument("--outside", nargs="*")
g.add_argument("--all", action="store_true")
p.add_argument("--ledger", action="store_true")
a = ap.parse_args()
ROOT = os.path.abspath(a.run_dir)
DELIV, REQS = os.path.join(ROOT, "deliverables"), os.path.join(ROOT, "INPUT", "requirements.md")
BOARD, SNAPS = os.path.join(ROOT, "work", "SCOREBOARD.md"), os.path.join(ROOT, "work", "snapshots")
COMP = os.path.join(DELIV, "01-requirements", "compliance.csv")
return globals()["cmd_" + a.cmd](a) or 0
if __name__ == "__main__":
sys.exit(main())
files/method/templates/increment-brief.md (33 wierszy)
# Brief <N>-<w> — <goal in one line> Run directory: <absolute run dir> · Tier: <concept|full> · Launched: <HH:MM> · **DEADLINE: <HH:MM>** (start wrapping up 2 minutes before it) ## Why this increment <the `seq.py rank` lines that chose it: id or folder, score, status, evidence quality, risk — copied, not rephrased> ## Goal <what must be true when you finish, as checkable statements: the number to compute and the requirement threshold it is compared with, the alternative comparison to make fair, the parts to source with prices, the folder to raise to fidelity n. Name the evidence quality wanted per id (estimate / calculation / simulation / sourced).> ## Requirement ids in focus - <ID> — now <status>, <quality>, risk <H/M/L> — <what would move it> - <IDS IN FOCUS of `seq.py quoters`: re-claim their value if a home they quote moves> ## WRITE SCOPE (relative to deliverables/; nothing else, ledger files never) - <file, folder/, or one calculation such as 02-concept/calc/power.*> - <ADD TO SCOPE of `seq.py quoters`: files that quote a home in this scope> ## Read first - work/BOUNDARIES.md, method/HYGIENE.md, handbook `.claude/skills/dd-hw-handbook/SKILL.md` §<n>, §<m> - deliverables/REPORT.md, work/SCOREBOARD.md, the files in your scope and the files they quote - <other package files this goal depends on> ## Done criteria - <specific, checkable result 1> - <specific, checkable result 2> - every number you add or change has its origin label; quotes outside a home carry `[=key@path]` - `python3 method/gates/check_all.py --tier <tier> --worker --scope "<scope>"` has no ERROR - `work/inc/<N>-<w>/claims.csv` has a row for every id in focus; `work/inc/<N>-<w>/note.md` written last
files/method/templates/skeleton-briefs.md (117 wierszy)
# Skeleton briefs Each `work/briefs/<id>.md` holds the header of `method/templates/increment-brief.md` (run directory, tier, launch time, DEADLINE), the ids the skeleton plan gives that worker, a pointer "Body: section <id> of this file" (the worker reads its section here; copying it is optional) and the "Done criteria". Increment 0 = 0-a, 0-b, 0-c (both tiers). Increment 1 = 1-a, 1-b, 1-c (full tier only, after increment 0 is GREEN). Skeleton fidelity (1) for every file below: it exists, it is truthful and labelled, it has a number wherever the design needs one (rough is fine; an ASSUMPTION range beats a blank), and it fits the files of the other two workers (names and subsystem rows from `work/SKELETON-PLAN.md`). Breadth before depth: never polish one file while another of your files does not exist yet. Report `FIDELITY <folder> 1` for each folder you wrote (2 only if it truly meets method/POLICY.md §4). --- ## 0-a — requirements and research WRITE SCOPE: `01-requirements/requirements.md`, `02-concept/physics-map.md`, `02-concept/benchmark.md` Read first: handbook §1–§4; `work/SKELETON-PLAN.md`; all of `INPUT/requirements.md`. Produce: - `requirements.md`: (1) given — one `### <ID> — <title>` section per id, in input order: the exact verification condition (temperatures, voltages, distances, duty, method) and the design consequence, two lines each; (2) derived — `DR-<AREA>-<nn>` entries covering the area list of handbook §2 (at least one line per area, or why it does not apply), each with parent, metric, target, threshold, verification method, origin; (3) conflicts and decisions, and the assumptions register. - `physics-map.md`: handbook §4 — the rating of every domain; for each CRITICAL one the governing relation, the dominant parameters, an order-of-magnitude number, the typical failure modes. - `benchmark.md`: three comparable products (the handbook's five are a later increment's goal) — the specifications that matter for the weight-4/5 requirements, price with date and URL, weak points users report; then the realistic target range and the differentiator in three lines. Claims: the ids the plan gives 0-a. ## 0-b — concept with a number for every subsystem WRITE SCOPE: `02-concept/concept.md`, `02-concept/alternatives.md`, `02-concept/subsystems.csv`, `02-concept/calc/`, `02-concept/risks.md` Read first: handbook §4–§6, §16; `work/SKELETON-PLAN.md`; the entries of your ids and of all weight-4/5 ids. Produce: - `subsystems.csv`, header exactly `subsystem,function,key_parts,power_w,mass_g,envelope_mm,cost_low,cost_high,currency,origin,source`: one row per subsystem (the plan's names; add what the concept needs). `power_w` = electrical power in the main operating mode (0 if none); `envelope_mm` = LxWxH; `cost_low`/`cost_high` = unit cost range at 1000 units; `key_parts` = real part numbers where a quick lookup finds them, else `ASSUMPTION: <generic part>`; `origin` = SOURCE / CALCULATION / ASSUMPTION (join several with +); `source` = URL with date, script path or a one-line reason. - `calc/budget.py` (reads `../subsystems.csv`, writes the totals of power, mass and cost range to `budget.results.json`) and one script per decisive estimate named in the plan, each starting with its hand estimate in a comment. - `alternatives.md`: the starting concept, A2 and A3 — principle, first-order numbers against the weight-4/5 requirements (with origins), main risks; knock-out by thresholds; weighted comparison using the requirement weights; one sensitivity line; why each loser loses. Numbers, not adjectives. - `concept.md` (handbook §5 order, compact): the product paragraph; functions and flows; the chosen architecture as a Mermaid block diagram; subsystems (pointing to subsystems.csv); interfaces; key estimates quoted with tags; three-line pointers to `safety.md`, `compliance-route.md` and `cost.md` (written in parallel by 0-c: do not quote their numbers yet); decisions taken without a human; open issues and the first steps of detailed design. - `risks.md`: the top risks — likelihood, impact, mitigation, the analysis or test that would retire each. Claims: the ids the plan gives 0-b, each with a predicted status, value and unit from your numbers. ## 0-c — cost, safety and compliance route WRITE SCOPE: `02-concept/cost.md`, `02-concept/safety.md`, `02-concept/compliance-route.md`, `09-bom-cost/cost.json` Read first: handbook §11, §12, §16; `work/SKELETON-PLAN.md`; `INPUT/brief.md`; the entries of your ids. Produce: - `cost.md`: a rough BOM with one line per subsystem of the plan (same names): candidate parts and prices read from supplier pages (with date) or ASSUMPTION ranges; unit cost at 1 / 100 / 1000 with every line of handbook §12; the retail price logic anchored on 2–3 comparable retail prices (date, URL) and a stated margin; say plainly if the numbers do not close. - `09-bom-cost/cost.json` in the RUN CONTRACT format: the home of the unit cost (cost.md quotes it with tags). - `safety.md`: the top hazards in the handbook §11 manner (hazard, situation, harm, risk, measures), the protections that act independently of firmware, the residual risks. - `compliance-route.md`: the EU and US rules that apply and why, each with SOURCE; the product-family standards found by research and the clauses that drive the design; the conformity route, lab tests, rough cost and time. Claims: the ids the plan gives 0-c. --- ## 1-a — mechanical skeleton (full tier) WRITE SCOPE: `03-cad/`, `04-drawings/`, `11-renders/` Read first: handbook §7, §8, §14; `INPUT/TOOLCHAIN.md` (code-CAD, STEP check, STEP to GLB, Blender, drawing route and its examples); `02-concept/concept.md`; `02-concept/subsystems.csv`. Produce: - `03-cad/`: parametric code-CAD on the route TOOLCHAIN.md marks as working; one script per designed part with its parameters at the top (envelopes from subsystems.csv), purchased parts as labelled envelope bodies, an assembly script; `assembly.step` and one STEP per designed part; `parts.csv` (`part,file,material,density_kg_m3,qty`); the STEP check output saved as `step-check.json` (validity, bounding box, interference). - `11-renders/`: `assembly.glb` converted from `assembly.step`; one PNG product view rendered from it in background mode; check what can be measured (file, resolution, object fills the frame). - `04-drawings/`: an assembly drawing with item numbers and a drawing of every manufactured part with overall dimensions, material and quantity (SVG or PDF); open each output to check it. Claims: size and mass ids (evidence: `03-cad/step-check.json`, `03-cad/parts.csv`). ## 1-b — electrical skeleton (full tier) WRITE SCOPE: `06-electronics/`, `07-firmware/` Read first: handbook §9, §10; `INPUT/TOOLCHAIN.md` (KiCad command line, PlatformIO platforms and versions); `02-concept/concept.md`, `02-concept/subsystems.csv`, `02-concept/safety.md`. Produce: - `06-electronics/`: a KiCad 9 project whose schematic covers power entry and protection, the controller and every electrical subsystem of subsystems.csv, with real parts (manufacturer part numbers) and matching symbols and footprints; the ERC report from the command line (JSON, file name containing `erc`); `power-budget.md` quoting subsystems.csv; a board is optional at this fidelity (with a DRC report if made). - `07-firmware/`: a PlatformIO project (platform and version exactly as TOOLCHAIN.md lists) with a state machine that has explicit FAULT and safe-default states, a watchdog, the pin map of the schematic, and one native host test of one interlock; it builds offline; no `.pio/` left inside deliverables. Claims: control, interface and standby-power ids. ## 1-c — numbers, safety file, BOM and test plan (full tier) WRITE SCOPE: `05-calculations/`, `08-safety-certification/`, `09-bom-cost/`, `10-prototype-test-plan/` Read first: handbook §6, §11, §12, §13; `02-concept/calc/`, `cost.md`, `safety.md`, `compliance-route.md`. Produce: - `05-calculations/`: the decisive calculations of the weight-4/5 requirements as standalone scripts, each writing its results file, deeper than the concept estimates. One home per number: never copy a `02-concept/calc` script; give a deeper result a name of its own (the concept value stays as the labelled concept-stage estimate) and quote and claim the new home from now on. - `08-safety-certification/`: the hazard analysis table with safety requirement ids traced to design measures; the regulatory map with SOURCE; the conformity route; what an accredited lab would test. - `09-bom-cost/`: `bom.csv` with the exact header of the RUN CONTRACT and real parts for every major item (price with the date read, else ASSUMPTION in `notes`); `cost.json` whose bom totals equal the bom.csv sums; `cost.md` with the roll-up at 1 / 100 / 1000. - `10-prototype-test-plan/`: the build plan outline, the bring-up outline with safety precautions, and one test per requirement id (method, equipment, conditions, pass criterion, the id it closes). Claims: cost, safety, certification and test-plan-verified ids.
files/method/templates/skeleton-plan.md (39 wierszy)
# Skeleton plan Written by the sequencer in the first minutes; the increment-0 workers read it. It fixes only what the three parallel workers must share to fit together; everything else is theirs to decide and record in the package. Tier: <concept|full> · START: <date output> · Final checks begin at: <clock time> · Run directory: <path> ## Product in one line <what it is, for whom, the one or two things it must do better than what exists (from INPUT/brief.md)> ## Top-weighted requirements (weight 4–5) | id | metric | threshold | why it shapes the concept | |---|---|---|---| ## Starting concept - Working principle: <the most conventional principle that plausibly meets every row above> - Why this one: <one line> · Weakest number: <the estimate most likely to break it, and the requirement it feeds> ## Alternatives for the comparison (different working principle or architecture, not cosmetics) - A2: <principle> — wins if <condition> - A3: <principle> — wins if <condition> ## Subsystems of the starting concept (the rows of 02-concept/subsystems.csv; names are quote keys) | subsystem | function | likely key part or technology | |---|---|---| ## Shared names (so parallel files fit) - currency <EUR unless the requirements use another> · cost quantities 1 / 100 / 1000 · SI units - calc scripts: `02-concept/calc/budget.py` (roll-up of subsystems.csv) plus one script per decisive estimate: <name.py — question> - unit cost home: `09-bom-cost/cost.json` (written by 0-c) ## Claim owners in increment 0 (every requirement id exactly once) | owner | ids | |---|---| | 0-a requirements & research | <ids whose evidence is an interpretation, a benchmark or a derived requirement> | | 0-b concept & numbers | <performance, function, size, mass, power, interface ids> | | 0-c cost, safety & compliance | <cost, price, safety, certification, manufacturing, documentation ids> |
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.