@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.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +10 -2
- package/checks/evidence.ts +63 -0
- package/evals/compose-probe-cable/case.json +160 -0
- package/evals/e2e-desk-rover/case.json +62 -0
- package/evals/e2e-desk-rover/fixture/AGENTS.md +9 -0
- package/evals/e2e-desk-rover/fixture/docs/brief.md +28 -0
- package/evals/e2e-desk-rover/fixture/docs/decisions.md +3 -0
- package/evals/e2e-desk-rover/fixture/package.json +1 -0
- package/evals/e2e-desk-rover/fixture/protoboard.config.ts +12 -0
- package/evals/e2e-desk-rover/fixture/src/.gitkeep +0 -0
- package/evals/e2e-desk-rover/solution/decisions.md +26 -0
- package/evals/e2e-desk-rover/solution/desk-rover.uhd.ts +60 -0
- package/evals/find-reversed-cell-leads/case.json +78 -0
- package/evals/fix-gnss-uart-swap/case.json +32 -0
- package/evals/fix-two-wiring-faults/case.json +97 -0
- package/evals/rough-draft-is-concept/case.json +36 -0
- package/evals/rough-draft-is-concept/fixture/AGENTS.md +9 -0
- package/evals/rough-draft-is-concept/fixture/docs/brief.md +8 -0
- package/evals/rough-draft-is-concept/fixture/docs/decisions.md +3 -0
- package/evals/rough-draft-is-concept/fixture/package.json +1 -0
- package/evals/rough-draft-is-concept/fixture/protoboard.config.ts +11 -0
- package/evals/rough-draft-is-concept/fixture/src/.gitkeep +0 -0
- package/evals/rough-draft-is-concept/solution/trail-light.uhd.ts +40 -0
- package/evals/tidy-the-diagram/case.json +30 -0
- package/evals/tidy-the-diagram/fixture/.protoboard/display.json +20 -0
- package/evals/tidy-the-diagram/fixture/AGENTS.md +9 -0
- package/evals/tidy-the-diagram/fixture/docs/brief.md +5 -0
- package/evals/tidy-the-diagram/fixture/package.json +1 -0
- package/evals/tidy-the-diagram/fixture/protoboard.config.ts +12 -0
- package/evals/tidy-the-diagram/fixture/src/bike-computer.uhd.ts +63 -0
- package/ops/bom.ts +22 -0
- package/package.json +41 -4
- package/protoboard-extension.json +65 -0
- package/skills/build-a-project/SKILL.md +562 -0
- package/skills/harness-author/SKILL.md +232 -0
- package/skills/harness-author/references/stack-up.md +70 -0
- package/skills/harness-author/references/wire-harnesses.md +35 -0
- package/skills/harness-author/scripts/stackup.ts +104 -0
- package/skills/harness-verify/SKILL.md +112 -0
- package/skills/system-compose/SKILL.md +269 -0
- package/skills/system-compose/references/diagnostic-codes.md +292 -0
- package/skills/system-compose/scripts/scenarios.ts +82 -0
- package/skills/system-review/SKILL.md +145 -0
- package/skills/test-and-measure/SKILL.md +181 -0
- package/skills/test-and-measure/references/procedure-template.md +45 -0
- package/skills/test-and-measure/references/test-plan.md +35 -0
- 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;
|