@protoboardai/ext-hardware 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +10 -2
  4. package/checks/evidence.ts +63 -0
  5. package/evals/compose-probe-cable/case.json +160 -0
  6. package/evals/e2e-desk-rover/case.json +62 -0
  7. package/evals/e2e-desk-rover/fixture/AGENTS.md +9 -0
  8. package/evals/e2e-desk-rover/fixture/docs/brief.md +28 -0
  9. package/evals/e2e-desk-rover/fixture/docs/decisions.md +3 -0
  10. package/evals/e2e-desk-rover/fixture/package.json +1 -0
  11. package/evals/e2e-desk-rover/fixture/protoboard.config.ts +12 -0
  12. package/evals/e2e-desk-rover/fixture/src/.gitkeep +0 -0
  13. package/evals/e2e-desk-rover/solution/decisions.md +26 -0
  14. package/evals/e2e-desk-rover/solution/desk-rover.uhd.ts +60 -0
  15. package/evals/find-reversed-cell-leads/case.json +78 -0
  16. package/evals/fix-gnss-uart-swap/case.json +32 -0
  17. package/evals/fix-two-wiring-faults/case.json +97 -0
  18. package/evals/rough-draft-is-concept/case.json +36 -0
  19. package/evals/rough-draft-is-concept/fixture/AGENTS.md +9 -0
  20. package/evals/rough-draft-is-concept/fixture/docs/brief.md +8 -0
  21. package/evals/rough-draft-is-concept/fixture/docs/decisions.md +3 -0
  22. package/evals/rough-draft-is-concept/fixture/package.json +1 -0
  23. package/evals/rough-draft-is-concept/fixture/protoboard.config.ts +11 -0
  24. package/evals/rough-draft-is-concept/fixture/src/.gitkeep +0 -0
  25. package/evals/rough-draft-is-concept/solution/trail-light.uhd.ts +40 -0
  26. package/evals/tidy-the-diagram/case.json +30 -0
  27. package/evals/tidy-the-diagram/fixture/.protoboard/display.json +20 -0
  28. package/evals/tidy-the-diagram/fixture/AGENTS.md +9 -0
  29. package/evals/tidy-the-diagram/fixture/docs/brief.md +5 -0
  30. package/evals/tidy-the-diagram/fixture/package.json +1 -0
  31. package/evals/tidy-the-diagram/fixture/protoboard.config.ts +12 -0
  32. package/evals/tidy-the-diagram/fixture/src/bike-computer.uhd.ts +63 -0
  33. package/ops/bom.ts +22 -0
  34. package/package.json +41 -4
  35. package/protoboard-extension.json +65 -0
  36. package/skills/build-a-project/SKILL.md +562 -0
  37. package/skills/harness-author/SKILL.md +232 -0
  38. package/skills/harness-author/references/stack-up.md +70 -0
  39. package/skills/harness-author/references/wire-harnesses.md +35 -0
  40. package/skills/harness-author/scripts/stackup.ts +104 -0
  41. package/skills/harness-verify/SKILL.md +112 -0
  42. package/skills/system-compose/SKILL.md +269 -0
  43. package/skills/system-compose/references/diagnostic-codes.md +292 -0
  44. package/skills/system-compose/scripts/scenarios.ts +82 -0
  45. package/skills/system-review/SKILL.md +145 -0
  46. package/skills/test-and-measure/SKILL.md +181 -0
  47. package/skills/test-and-measure/references/procedure-template.md +45 -0
  48. package/skills/test-and-measure/references/test-plan.md +35 -0
  49. package/skills/test-and-measure/scripts/test-data.ts +94 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Fault scenarios: run the system checks on deliberately broken variants of
3
+ * the project's system and report whether each fault is caught.
4
+ *
5
+ * npx tsx <this file> [src/scenarios.ts] [--json]
6
+ *
7
+ * The scenarios file exports `SCENARIOS`, each
8
+ * { id, label, description?, expect: string[], system?: ModuleDef, overrides?: ModuleDef[] }
9
+ * where `system` defaults to the project root, `overrides` replace
10
+ * definitions by id (e.g. a frame cut for the wrong stack, a board with a
11
+ * pin on the wrong net; every board below the system is checked on its own
12
+ * nets, as `protoboard check` does), and `expect`
13
+ * lists the diagnostic codes the fault must raise at error or warning
14
+ * severity (e.g. "UHD_LINK_STATE", "UHD_BUS_ADDRESS", "UHD_ASSEMBLY").
15
+ * A scenario with `expect: []` (the nominal build) must raise no errors.
16
+ * Boards below the root (definitions that declare nets) get the system rules
17
+ * on their own nets too, as `protoboard check` runs them.
18
+ * Exit 1 when a fault goes uncaught or the nominal build has errors.
19
+ */
20
+ import { resolve } from "node:path";
21
+ import { pathToFileURL } from "node:url";
22
+ import { assemble, checkSystem, type ModuleDef, type ModuleLookup } from "@deltarobotics/uhd";
23
+ import { boardModules, loadProject } from "@protoboardai/core";
24
+
25
+ interface Scenario {
26
+ id: string;
27
+ label: string;
28
+ description?: string;
29
+ expect: string[];
30
+ system?: ModuleDef;
31
+ overrides?: ModuleDef[];
32
+ }
33
+
34
+ const args = process.argv.slice(2);
35
+ const json = args.includes("--json");
36
+ const file = resolve(args.find((a) => !a.startsWith("--")) ?? "src/scenarios.ts");
37
+ const p = await loadProject(process.cwd(), { checks: false, extensions: false });
38
+ if (!p.rootDef) throw new Error("the project has no root system");
39
+ const { SCENARIOS } = (await import(pathToFileURL(file).href)) as { SCENARIOS?: Scenario[] };
40
+ if (!SCENARIOS?.length) throw new Error(`${file} exports no SCENARIOS`);
41
+
42
+ // assembly is rooted at the first top-level instance with interface frames (as `protoboard check` does)
43
+ const structure = p.instances.find((i) => i.parent === "" && p.lookup(i.definition)?.interfaces.some((f) => f.geometry?.frame));
44
+
45
+ const results = SCENARIOS.map((s) => {
46
+ const byId = new Map((s.overrides ?? []).map((d) => [d.id, d]));
47
+ const lookup: ModuleLookup = (id) => byId.get(id) ?? p.lookup(id);
48
+ const system = s.system ?? p.rootDef!;
49
+ const found: { code: string; severity: string; message: string }[] = checkSystem(system, lookup).diagnostics.map((d) => ({
50
+ code: `UHD_${d.rule.toUpperCase()}`,
51
+ severity: d.severity,
52
+ message: d.message,
53
+ }));
54
+ // the boards below the system, each checked on its own nets (as `protoboard check` does: the root covers the
55
+ // tree for categories and passive overrides; a board is powered from outside it)
56
+ for (const { def, path } of boardModules(system, lookup)) {
57
+ for (const d of checkSystem(def, lookup).diagnostics) {
58
+ if (d.rule === "unknown_category" || d.rule === "passive_override" || d.rule === "unpowered") continue;
59
+ found.push({ code: `UHD_${d.rule.toUpperCase()}`, severity: d.severity, message: `${path}: ${d.message}` });
60
+ }
61
+ }
62
+ if (structure) {
63
+ for (const i of assemble(system, lookup, { root: [structure.path] }).issues) {
64
+ found.push({ code: i.severity === "info" ? "UHD_UNPLACED" : "UHD_ASSEMBLY", severity: i.severity, message: `${i.subject}: ${i.message}` });
65
+ }
66
+ }
67
+ const raised = found.filter((d) => d.severity !== "info");
68
+ const missed = s.expect.filter((c) => !raised.some((d) => d.code === c));
69
+ const errors = found.filter((d) => d.severity === "error");
70
+ const ok = s.expect.length ? missed.length === 0 : errors.length === 0;
71
+ return { id: s.id, label: s.label, expect: s.expect, ok, missed, raised: raised.map((d) => `${d.severity} ${d.code}: ${d.message}`) };
72
+ });
73
+
74
+ if (json) console.log(JSON.stringify(results, null, 2));
75
+ else {
76
+ for (const r of results) {
77
+ console.log(`${r.ok ? "caught " : "MISSED "} ${r.id.padEnd(22)} ${r.label}`);
78
+ if (!r.ok && r.expect.length) console.log(` expected ${r.missed.join(", ")}`);
79
+ for (const line of r.raised.filter((l) => (r.expect.length ? r.expect.some((c) => l.includes(c)) : l.startsWith("error")))) console.log(` ${line.slice(0, 180)}`);
80
+ }
81
+ }
82
+ process.exitCode = results.every((r) => r.ok) ? 0 : 1;
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: system-review
3
+ description: Independent review of a composed UHD system against its brief — every requirement traced to a check, document value or test; every link, harness and mate justified by both parts' interfaces; every diagnostic dispositioned; fault scenarios caught. Run by a fresh agent after system-compose (or before a release); writes a findings file and never edits the model itself.
4
+ ---
5
+
6
+ # System review
7
+
8
+ A fresh agent, with no memory of how the system was built, checks that it
9
+ does what the brief says and that the model says only what the sources
10
+ support. The composer's reasoning is not evidence; the files are.
11
+
12
+ ## Purpose
13
+
14
+ Catch what the composer cannot see in their own work: requirements nothing
15
+ shows, links that pass only by inference, warnings quietly accepted, gaps
16
+ presented as facts. Use it when **system-compose** hands off (0 errors,
17
+ scenarios caught), before a release, and after any large change to the
18
+ system. In **build-a-project** it closes stage 4 and is repeated in stage 8.
19
+ It never edits the model; findings go back to the composer.
20
+
21
+ ## Inputs
22
+
23
+ | Input | Where | From |
24
+ | --- | --- | --- |
25
+ | Brief, decisions, requirement → evidence mapping | `docs/brief.md`, `docs/decisions.md` (or the test plan) | **system-compose** step 1 |
26
+ | Check results | `npx protoboard check --format json` | the project |
27
+ | The system | `npx protoboard inspect --format json`, `npx protoboard inspect <instance> --format json` | the project |
28
+ | Scenario run | `npx tsx node_modules/@protoboardai/ext-hardware/skills/system-compose/scripts/scenarios.ts --json` | **system-compose** step 4 |
29
+ | Part evidence | `parts/evidence/<id>/`, catalog origins in `parts/origins.json`, catalog files in `parts/catalog/<id>/` | the part skills, `parts add` |
30
+
31
+ ## Steps
32
+
33
+ ### 1. Requirements
34
+
35
+ Each requirement in the brief has one of: a validator that ran and passes, a
36
+ document value, a planned test, or an explicit "not verified" entry. A
37
+ requirement with none is a finding. Model checks never count as physical
38
+ acceptance.
39
+
40
+ ### 2. Coverage
41
+
42
+ ```bash
43
+ npx protoboard check --severity info --format json
44
+ ```
45
+
46
+ In `checks[]`, every validator ran. A skipped validator, or a check `reason`
47
+ about unchecked refs, is unknown coverage: list it.
48
+
49
+ ### 3. Links
50
+
51
+ For a sample of links (all power links, every link to the main controller,
52
+ every mechanical mate), open both interfaces:
53
+
54
+ ```bash
55
+ npx protoboard inspect <instance> --format json
56
+ ```
57
+
58
+ Confirm the parameters that DRC compared come from the parts' sources, and
59
+ that the connection exists in the hardware (pinout, manual wiring diagram).
60
+ Links that pass only by inference (`valid_inferred`) or manual override need a
61
+ reason.
62
+
63
+ ### 4. Harnesses
64
+
65
+ Every electrical link that leaves a board is carried by a harness or a stated
66
+ solder joint; every mechanical joint has a fastener stack whose arithmetic you
67
+ re-derive (**harness-verify**).
68
+
69
+ ### 5. Diagnostics
70
+
71
+ Every warning has a disposition in `docs/decisions.md` or an open item; no
72
+ parameter was removed or widened to make a rule pass (compare with the part's
73
+ sources).
74
+
75
+ ### 6. Scenarios
76
+
77
+ ```bash
78
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/system-compose/scripts/scenarios.ts --json
79
+ ```
80
+
81
+ All caught; the nominal build raises no errors. Think of one realistic
82
+ mistake the scenarios do not cover and say whether the checks would catch it.
83
+
84
+ ### 7. Assumptions and gaps
85
+
86
+ List every `assumption` and `data_gap` trait in local definitions:
87
+
88
+ ```bash
89
+ npx protoboard build # then search .protoboard/build/resolved.json, or grep src/ and parts/local/
90
+ ```
91
+
92
+ Each is either acceptable for this revision or an open item.
93
+
94
+ ### 8. Write the review
95
+
96
+ `docs/reviews/system-review-<YYYY-MM-DD>.md`:
97
+
98
+ ```md
99
+ # System review — <project> @ <git revision>
100
+ Reviewer: <agent/person> · check: <E> errors, <W> warnings · scenarios: <caught>/<total>
101
+
102
+ | id | severity | entity | finding | evidence | suggested fix |
103
+ | --- | --- | --- | --- | --- | --- |
104
+ | R1 | major | link dji_fc (derived bec_10v ↔ vcc) | ... | O4 manual p.x | ... |
105
+
106
+ ## Requirement trace
107
+ | requirement | shown by | status |
108
+ ```
109
+
110
+ Hand the file to the composer; do not edit the model in the review.
111
+
112
+ ## Decision points
113
+
114
+ | Decision | Rule |
115
+ | --- | --- |
116
+ | Severity | **blocker**: wrong hardware would be built or damaged. **major**: a claim not supported by a source. **minor**: traceability, naming. |
117
+ | Sample size for links | All power links, all links to the main controller, all mates; plus at least three others chosen at random. |
118
+ | A requirement shown only by a model check | Acceptable for the model; list the physical test that would confirm it, or "not verified". |
119
+ | Unknown coverage | Always a finding (minor unless it hides a requirement). |
120
+
121
+ ## Outputs
122
+
123
+ | Output | Where |
124
+ | --- | --- |
125
+ | The review | `docs/reviews/system-review-<YYYY-MM-DD>.md` |
126
+
127
+ ## Verification
128
+
129
+ The review file exists, names the git revision and the check and scenario
130
+ counts, has a row for every requirement in the brief, and every finding cites
131
+ its evidence (a file and line, a source page, a command output). No model
132
+ file changed (`git status --short src/ parts/` shows nothing from the review).
133
+
134
+ ## Failure handling
135
+
136
+ | Code or symptom | Means | Do |
137
+ | --- | --- | --- |
138
+ | `checks[]` has a `skipped` validator | Unknown coverage | Record it; ask the composer to fix the reason (a missing tool, an upstream error) |
139
+ | Errors in `check` | The system was not ready for review | Stop; hand back to **system-compose** |
140
+ | `UHD_LINK_STATE` warning (`valid_inferred`) | DRC matched by inference | Check the datasheets yourself; a finding unless a reason is recorded |
141
+ | A warning with no disposition | Accepted silently | Finding (major) |
142
+ | A scenario `MISSED` | The check the design relies on does not fire | Finding (major or blocker) |
143
+ | `HW_NO_SOURCES`, `HW_NOT_VERIFIED` | Part evidence missing | Finding; the part goes back to **uhd-part-research** / **uhd-part-verify** |
144
+
145
+ Every other code: **system-compose** [diagnostic codes](../system-compose/references/diagnostic-codes.md).
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: test-and-measure
3
+ description: Plan and record physical tests for a UHD system — derive a test plan from the brief's requirements and the model (first-article inspection, board bring-up, functional checks; what each test proves, pass criteria from model values), write step-by-step procedures, and record results as measured test-data files (UHD tech-docs schema, status standin|measured) that replace stand-in data in the documents. Use when a design needs acceptance evidence, before building a prototype, and whenever measurements come back from the bench.
4
+ ---
5
+
6
+ # Test and measure
7
+
8
+ ```
9
+ brief requirements ─┐
10
+ UHD model values ───┼─► docs/test-plan.md ─► docs/procedures/<test-id>.md ─► bench / flight
11
+ stand-in data ──────┘ │
12
+ docs/test-data/<test-id>.json (status: measured) ◄┘
13
+ │
14
+ documents rebuilt: stand-in watermark gone, values marked M
15
+ ```
16
+
17
+ Model checks validate the model. Only measurements on a physical unit,
18
+ recorded against a release and a unit, establish that the hardware meets its
19
+ brief. Never write a measured file for a test that was not run.
20
+
21
+ ## Purpose
22
+
23
+ Turn requirements and model claims into tests a person can run, and turn
24
+ their results into evidence the documents and the release carry. Use it when
25
+ a design needs acceptance evidence, before a prototype is built (the plan
26
+ goes into the Manufacturing Package's test and validation plan), and
27
+ whenever measurements come back. In **build-a-project** it is part of stage
28
+ 10; recording results happens after the release, against it. This skill
29
+ plans and records; a person performs every physical test.
30
+
31
+ ## Inputs
32
+
33
+ | Input | Where | From |
34
+ | --- | --- | --- |
35
+ | Requirements and their "shown by" mapping | `docs/brief.md`, `docs/decisions.md` | **system-compose** step 1 |
36
+ | Model values and warnings | `npx protoboard check --format json`, `npx protoboard inspect --format json` | the project |
37
+ | Stand-in data the documents use | `docs/test-data/*.json` with `status: "standin"` | **uhd-tech-docs** (`protoboard run tech-docs`) |
38
+ | Firmware items to verify | the firmware reference's UNVERIFIED list, the generator's warnings | **firmware-config** |
39
+ | Bench results | raw logs, CSVs, photos from the person who ran the test | the bench |
40
+
41
+ ## Steps
42
+
43
+ ### 1. Test plan from requirements and the model
44
+
45
+ `docs/test-plan.md`, one row per test ([template and the quad's plan](references/test-plan.md)):
46
+
47
+ | Column | Content |
48
+ | --- | --- |
49
+ | `id` | kebab-case; **the same id as its test-data file** (`hover-current`) |
50
+ | requirement | the brief line(s) it proves, quoted |
51
+ | model claim | the value queries it checks, e.g. `sys:checks.diagnostics[rule=propulsion_current].details.totalPeak`, `def:<part>:…` — the numbers the documents print |
52
+ | level | inspection · bench · propulsion · flight |
53
+ | pass criterion | from the requirement, or the model value with a stated tolerance; never invented after the result |
54
+ | depends on | tests that must pass first (power-up before props on) |
55
+ | status | planned · procedure written · run (pass/fail) · blocked |
56
+
57
+ Derive tests systematically:
58
+
59
+ - every requirement in the brief → at least one test or an explicit
60
+ "not tested in this revision";
61
+ - every warning and every `assumption` / `data_gap` the model carries → a
62
+ measurement that would retire it (fastener torques, frame mass, the
63
+ propulsion-current warning → a current log at full throttle);
64
+ - every stand-in data file the documents use (`status: "standin"`) → the test
65
+ that replaces it, with the same id and columns;
66
+ - every firmware setting marked verify (motor order, compass alignment) → a
67
+ bench check (**firmware-verify**);
68
+ - the Manufacturing Package's basic validation plan
69
+ (`docs/specs/release-package-and-evals.md` §2 in the stack repository):
70
+ **first-article inspection** (dimensions, fits, seals against the CAD and
71
+ the enclosure check's report); **board bring-up** for each custom board
72
+ (rails against the board's nets, idle current against the supply budget,
73
+ programming and boot, bus scans with the expected I2C addresses from the
74
+ parts, a smoke test per peripheral); **functional checks** per requirement;
75
+ and the records each one captures.
76
+
77
+ Order by risk and by what destroys hardware first: inspection → power-up with
78
+ a current limit, no props or loads → rails and peripherals → motor direction
79
+ and order → thrust stand → hover → endurance.
80
+
81
+ ### 2. Procedures
82
+
83
+ `docs/procedures/<test-id>.md` ([template](references/procedure-template.md)):
84
+ preconditions (release/revision, unit serial, firmware dump hash, battery
85
+ state), equipment with calibration date, safety and abort criteria, numbered
86
+ steps, what to record and in which raw file, the pass criterion, and the
87
+ test-data columns the result fills. A procedure is runnable by someone who has
88
+ not read the model.
89
+
90
+ ### 3. Recording results
91
+
92
+ One file per test: `docs/test-data/<test-id>.json`, schema
93
+ `uhd-test-data/1` (`node_modules/@protoboardai/ext-docs/skills/uhd-tech-docs/templates/test-data.schema.json`,
94
+ guide `…/uhd-tech-docs/references/test-data.md`).
95
+
96
+ - `status: "measured"`; `subject.system` and the tested `modules`;
97
+ - `conditions[]`: battery voltage, ambient, prop, firmware, all-up weight…
98
+ as measured on the day (units, `""` for text);
99
+ - `provenance`: `method` (the procedure, one paragraph), `date` (ISO),
100
+ `operator` and/or `instruments[]`, `rawFiles[]` (logs, CSVs, photos; keep
101
+ them next to the file or in a data store they can be fetched from); no
102
+ `derivedFrom` (that is stand-in provenance);
103
+ - `columns`, `rows`, `summary`: **the same ids and units as the stand-in it
104
+ replaces**, so documents pick it up unchanged. A new test gets a new id.
105
+
106
+ Validate before committing:
107
+
108
+ ```bash
109
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/test-and-measure/scripts/test-data.ts docs/test-data
110
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/test-and-measure/scripts/test-data.ts docs/test-data/hover-current.json --replaces <standin dir or file>
111
+ ```
112
+
113
+ It fails on template placeholders (all-zero summaries, "EXAMPLE ONLY"
114
+ method), a missing date or operator, rows that do not match the columns, and
115
+ (with `--replaces`) changed ids, columns or units.
116
+
117
+ A failed test is data too: record it as measured with its conditions, then
118
+ open the design issue it exposes. Measured results never rewrite the design
119
+ specification; they feed the next revision (**system-compose**).
120
+
121
+ ### 4. Replacing stand-ins in the documents
122
+
123
+ Documents are built by **uhd-tech-docs** (`@protoboardai/ext-docs`) from the
124
+ project's `docs/test-data/`; a measured file with the stand-in's id wins, the
125
+ stand-in generator never overwrites it, charts lose the STAND-IN watermark and
126
+ values get the **M** marker.
127
+
128
+ ```bash
129
+ npx protoboard run tech-docs
130
+ npx protoboard run tech-docs-verify # fails if a measured chart still carries a watermark
131
+ ```
132
+
133
+ Then the visual review in **uhd-tech-docs-verify**.
134
+
135
+ ### 5. Tie results to a release
136
+
137
+ Every measured file names what was tested: put the release revision and unit
138
+ serial in `conditions` (`{ "name": "Release", "value": "quad5-r1", "unit": "" }`,
139
+ `{ "name": "Unit", "value": "SN001", "unit": "" }`). See **release**: measured
140
+ results attach to a release later as evidence and never change its frozen
141
+ inputs.
142
+
143
+ ## Decision points
144
+
145
+ | Decision | Rule |
146
+ | --- | --- |
147
+ | Test or "not tested" | Every requirement gets a test, or an explicit "not tested in this revision" with the reason. |
148
+ | Pass criterion | From the requirement, or the model value with a stated tolerance, written before the test is run. |
149
+ | Order | What can destroy hardware is tested last and behind its prerequisites; first power always through a current limit. |
150
+ | New id or same id | A result that replaces a stand-in keeps its id, columns and units; anything else is a new test with a new id. |
151
+ | A failed test | Record it as measured; open the design issue; do not edit the model to match it in the same revision. |
152
+
153
+ ## Outputs
154
+
155
+ | Output | Where |
156
+ | --- | --- |
157
+ | Test plan | `docs/test-plan.md` |
158
+ | Procedures | `docs/procedures/<test-id>.md` |
159
+ | Measured results and raw files | `docs/test-data/<test-id>.json`, raw files next to them or in a fetchable store |
160
+
161
+ ## Verification
162
+
163
+ ```bash
164
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/test-and-measure/scripts/test-data.ts docs/test-data # exit 0
165
+ npx protoboard run tech-docs-verify # succeeds after a rebuild
166
+ ```
167
+
168
+ Every requirement has a row in the plan; every stand-in file has a test with
169
+ its id; every measured file names its release and unit.
170
+
171
+ ## Failure handling
172
+
173
+ | Code or symptom | Means | Do |
174
+ | --- | --- | --- |
175
+ | `test-data.ts` fails on placeholders | A template value was left in | Replace it with the measurement, or do not commit the file |
176
+ | `test-data.ts --replaces` reports changed ids, columns or units | The result would not replace the stand-in | Match the stand-in's columns, or give the test a new id |
177
+ | `tech-docs-verify` "standin" finding | A measured chart still has a watermark, or a stand-in value lacks its marker | Rebuild the documents (`run tech-docs`) |
178
+ | `UHD_PROPULSION_CURRENT`, `UHD_SUPPLY_BUDGET` warnings | A model claim that needs a measurement | Plan the test that retires it |
179
+ | `UHD_FASTENER_TORQUE` warnings | Torques are assumptions | Plan the torque inspection |
180
+
181
+ Every other code: **system-compose** [diagnostic codes](../system-compose/references/diagnostic-codes.md).
@@ -0,0 +1,45 @@
1
+ # Procedure template
2
+
3
+ ```md
4
+ # <test-id>: <title>
5
+
6
+ Plan row: docs/test-plan.md#<test-id> · Result file: docs/test-data/<test-id>.json
7
+
8
+ ## Preconditions
9
+ - Release / revision: <release id or git revision>; unit: <serial>
10
+ - Firmware: <version>, configuration dump <file> (sha256 <…>)
11
+ - Battery: <pack id>, resting voltage <V> (record it in conditions)
12
+ - Tests passed first: <ids>
13
+
14
+ ## Equipment
15
+ | instrument | model | range / resolution | calibration date |
16
+
17
+ ## Safety and abort
18
+ - Props off unless the step says otherwise; current-limited supply for first power.
19
+ - Abort if: <smoke, current above X A, temperature above Y °C, loss of link>.
20
+
21
+ ## Steps
22
+ 1. …
23
+ 2. Record <quantity> in <raw file> (column `<id>`, unit <unit>).
24
+
25
+ ## Pass criterion
26
+ <quoted from the plan; decided before the test>
27
+
28
+ ## Result
29
+ Write docs/test-data/<test-id>.json (status "measured", same columns as the
30
+ stand-in), validate it with scripts/test-data.ts, and list the raw files in
31
+ provenance.rawFiles. Record failures the same way.
32
+ ```
33
+
34
+ ## Example: hover-current (quadcopter-5in)
35
+
36
+ - Preconditions: `motor-order-direction` passed; props fitted per the
37
+ assembly guide; all-up weight measured on a scale (condition "All-up weight
38
+ (weighed)").
39
+ - Equipment: FC current sensor, calibrated against a clamp meter at 3
40
+ currents; scale 0.1 g.
41
+ - Steps: hover in still air 60 s per ballast step; Blackbox logging on;
42
+ average current over the steady 40 s.
43
+ - Columns (must match the stand-in): `auw` g, `thrust_motor` g,
44
+ `current_total` A, `power_total` W; summary `auw_g`, `hover_current_A`,
45
+ `hover_power_W`, `avionics_current_A`.
@@ -0,0 +1,35 @@
1
+ # Test plan template, with the quadcopter's first rows
2
+
3
+ ```md
4
+ # Test plan — <project> (<revision>)
5
+
6
+ Scope: what this plan accepts and what it does not (e.g. "bench and hover;
7
+ no endurance or crash testing in this revision").
8
+
9
+ | id | requirement | model claim (value queries) | level | pass criterion | depends on | status |
10
+ | --- | --- | --- | --- | --- | --- | --- |
11
+ ```
12
+
13
+ ## Quadcopter-5in: first rows (derived from `docs/brief.md` and the model)
14
+
15
+ Requirements quoted from the brief: "True-X 5-inch … ≥ 5 mm prop clearance",
16
+ "6S LiPo power (CNHL 1100 mAh 100C)", "Acceptance: … the unit is assembled per
17
+ the generated guide, and bench/flight measurements replace the stand-in data
18
+ in the datasheet."
19
+
20
+ | id | requirement | model claim | level | pass criterion | depends on |
21
+ | --- | --- | --- | --- | --- | --- |
22
+ | `frame-inspection` | frame fabricated from its CAD | frame CAD: plate DXF, `frames.json`, feature manifest; hole Ø = fastener + 0.2 mm | inspection | every hole pattern within ±0.1 mm of the DXF (caliper/pin gauge); plate thickness as `plate_thickness_mm` | — |
23
+ | `prop-clearance` | ≥ 5 mm prop clearance | wheelbase and prop diameter (frame metadata, prop part) | inspection | measured tip-to-tip gap ≥ 5 mm on all adjacent pairs | `frame-inspection` |
24
+ | `fastener-torque` | assembled per the guide | every `fastenerStack` torque (all assumptions today) | inspection | torque driver at the model value; no stripped thread, no crushed grommet; record any change as a new assumption | `frame-inspection` |
25
+ | `power-up` | 6S power | supply budgets per rail (`UHD_SUPPLY_BUDGET` details) | bench | current-limited supply, no props: idle current within the budget; BEC rails 5 V / 10 V / 4.5 V within the parts' stated tolerance | `fastener-torque` |
26
+ | `peripherals` | ELRS, O4, GNSS/compass | link `rx_crsf`; cables `dji_fc`/`dji_o4` (derived UART5 ↔ MSP) and `gnss_socket`/`gnss_pads` (derived UART1, I2C1); firmware CLI | bench | receiver link, OSD visible in goggles, GPS fix, compass detected (**firmware-verify**) | `power-up` |
27
+ | `motor-order-direction` | Quad-X, correct spin | Betaflight Quad-X order; motor/prop spin | bench | Motors tab: M1…M4 at rear-right, front-right, rear-left, front-left; directions match the props | `peripherals` |
28
+ | `throttle-sweep` | replaces the datasheet stand-in | stand-in `throttle-sweep` (same columns) | propulsion | curve recorded on a thrust stand; within ±15 % of the maker's points or a design issue is opened | `motor-order-direction` |
29
+ | `hover-current` | replaces the stand-in | stand-in `hover-current`; all-up weight | flight | measured hover current and AUW recorded; weigh before flight | `throttle-sweep` |
30
+ | `propulsion-peak-current` | retire `UHD_PROPULSION_CURRENT` | `details.totalPeak` 171.88 A vs pack 110 A continuous / 220 A burst | flight | Blackbox current log of full-throttle punches; decision recorded (accept bursts / limit current) | `hover-current` |
31
+
32
+ Stand-in files the documents currently use (the project's
33
+ `docs/test-data/`, written by `protoboard run tech-docs`): `throttle-sweep`,
34
+ `hover-current`, `flight-time`, `thrust-to-weight`, `esc-temp-punchout`,
35
+ `battery-discharge`. Each needs a row whose id matches.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Validate test-data files (UHD tech-docs convention, schema uhd-test-data/1:
3
+ * @protoboardai/ext-docs skills/uhd-tech-docs/templates/test-data.schema.json).
4
+ *
5
+ * npx tsx <this file> <file-or-dir> [--replaces <standin-file-or-dir>] [--json]
6
+ *
7
+ * Checks every <id>.json: required fields, id == file name, status
8
+ * standin|measured, measured files carry provenance.date (and operator or
9
+ * instruments), rows match the columns, units present, no placeholder zeros
10
+ * left in a measured file's summary. With --replaces, a measured file must
11
+ * keep the stand-in's id, column ids and units so documents pick it up
12
+ * unchanged. Exit 1 on any error.
13
+ */
14
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
15
+ import { basename, join } from "node:path";
16
+
17
+ interface TestData {
18
+ id: string;
19
+ title: string;
20
+ status: "standin" | "measured";
21
+ subject: { system: string; modules?: string[] };
22
+ conditions: { name: string; value: number | string; unit: string }[];
23
+ provenance: { method: string; date?: string; operator?: string; instruments?: string[]; rawFiles?: string[]; derivedFrom?: string[]; assumptions?: string[] };
24
+ columns: { id: string; label: string; unit: string }[];
25
+ rows: (number | string)[][];
26
+ summary: { id: string; label: string; value: number | string; unit: string }[];
27
+ }
28
+
29
+ const args = process.argv.slice(2);
30
+ const json = args.includes("--json");
31
+ const replacesAt = args.indexOf("--replaces");
32
+ const replaces = replacesAt >= 0 ? args[replacesAt + 1] : undefined;
33
+ const target = args.find((a, k) => !a.startsWith("--") && (replacesAt < 0 || k !== replacesAt + 1));
34
+ if (!target) throw new Error("usage: test-data.ts <file-or-dir> [--replaces <standin-file-or-dir>] [--json]");
35
+
36
+ const files = (path: string) => (statSync(path).isDirectory() ? readdirSync(path).filter((f) => f.endsWith(".json")).sort().map((f) => join(path, f)) : [path]);
37
+
38
+ function validate(file: string): string[] {
39
+ const errs: string[] = [];
40
+ let d: TestData;
41
+ try {
42
+ d = JSON.parse(readFileSync(file, "utf8"));
43
+ } catch (e) {
44
+ return [`not JSON: ${(e as Error).message}`];
45
+ }
46
+ for (const k of ["id", "title", "status", "subject", "conditions", "provenance", "columns", "rows", "summary"] as const) if (d[k] === undefined) errs.push(`missing ${k}`);
47
+ if (errs.length) return errs;
48
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(d.id)) errs.push(`id "${d.id}" must be kebab-case`);
49
+ if (d.id !== basename(file, ".json")) errs.push(`id "${d.id}" must equal the file name`);
50
+ if (d.status !== "standin" && d.status !== "measured") errs.push(`status must be standin or measured, not "${d.status}"`);
51
+ if (!d.subject.system) errs.push("subject.system is required");
52
+ if (!d.provenance.method) errs.push("provenance.method is required");
53
+ if (!d.columns.length) errs.push("at least one column");
54
+ for (const c of [...d.columns, ...d.summary, ...d.conditions]) if (typeof c.unit !== "string") errs.push(`"${"id" in c ? c.id : c.name}" has no unit (use "" for text)`);
55
+ d.rows.forEach((r, k) => {
56
+ if (r.length !== d.columns.length) errs.push(`row ${k} has ${r.length} values for ${d.columns.length} columns`);
57
+ });
58
+ if (d.status === "measured") {
59
+ if (!d.provenance.date || !/^\d{4}-\d\d-\d\d/.test(d.provenance.date)) errs.push("a measured file needs provenance.date (ISO date of the test)");
60
+ if (!d.provenance.operator && !d.provenance.instruments?.length) errs.push("a measured file needs provenance.operator or provenance.instruments");
61
+ if (/EXAMPLE ONLY/i.test(d.provenance.method)) errs.push("provenance.method is still the template's EXAMPLE ONLY text");
62
+ if (d.summary.length && d.summary.every((s) => s.value === 0)) errs.push("every summary value is 0: template placeholders, not measurements");
63
+ if (d.provenance.derivedFrom?.length) errs.push("a measured file must not carry derivedFrom (that is stand-in provenance)");
64
+ } else if (!d.provenance.derivedFrom?.length) {
65
+ errs.push("a stand-in needs provenance.derivedFrom (the model queries it was derived from)");
66
+ }
67
+ return errs;
68
+ }
69
+
70
+ function compare(measured: string, standin: string): string[] {
71
+ const m = JSON.parse(readFileSync(measured, "utf8")) as TestData;
72
+ const s = JSON.parse(readFileSync(standin, "utf8")) as TestData;
73
+ const errs: string[] = [];
74
+ if (m.id !== s.id) errs.push(`id ${m.id} vs stand-in ${s.id}`);
75
+ const cols = (d: TestData) => d.columns.map((c) => `${c.id}[${c.unit}]`).join(", ");
76
+ if (cols(m) !== cols(s)) errs.push(`columns ${cols(m)} vs stand-in ${cols(s)}`);
77
+ const sums = (d: TestData) => new Map(d.summary.map((x) => [x.id, x.unit]));
78
+ for (const [id, unit] of sums(s)) if (sums(m).get(id) !== unit) errs.push(`summary ${id} [${unit}] missing or in other units`);
79
+ return errs;
80
+ }
81
+
82
+ const results = files(target).map((f) => {
83
+ const errors = validate(f);
84
+ if (replaces && !errors.length) {
85
+ const other = statSync(replaces).isDirectory() ? join(replaces, basename(f)) : replaces;
86
+ if (existsSync(other)) errors.push(...compare(f, other).map((e) => `replaces ${other}: ${e}`));
87
+ else errors.push(`no stand-in ${other} to replace`);
88
+ }
89
+ return { file: f, status: (() => { try { return JSON.parse(readFileSync(f, "utf8")).status; } catch { return undefined; } })(), errors };
90
+ });
91
+
92
+ if (json) console.log(JSON.stringify(results, null, 2));
93
+ else for (const r of results) console.log(`${r.errors.length ? "FAIL" : "ok "} ${r.file} (${r.status ?? "?"})${r.errors.map((e) => `\n ${e}`).join("")}`);
94
+ process.exitCode = results.some((r) => r.errors.length) ? 1 : 0;