@protoboardai/ext-release 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/lib/units.ts ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Package units (release-package-and-evals.md §2, "Splitting by module"):
3
+ * the root system is always a unit; each `release.units` entry (an instance
4
+ * path, or a module id meaning every instance of that module) is another;
5
+ * every other instance folds into the nearest unit above it.
6
+ */
7
+ import type { Instance, Project } from "@protoboardai/core";
8
+ import type { SectionDiagnostic, Unit, UnitMember } from "./types.ts";
9
+
10
+ export const ROOT_UNIT = "system";
11
+
12
+ const parentOf = (path: string) => (path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "");
13
+ const relTo = (base: string, path: string) => (base === "" ? path : path.slice(base.length + 1));
14
+
15
+ /** Units in the order of work: units inside other units first, the root (final assembly) last. */
16
+ export function resolveUnits(p: Project, entries: string[] = []): { units: Unit[]; diagnostics: SectionDiagnostic[] } {
17
+ const diagnostics: SectionDiagnostic[] = [];
18
+ const err = (message: string) => diagnostics.push({ code: "REL_UNIT_INVALID", severity: "error", message });
19
+ if (!p.rootDef) return { units: [], diagnostics: [{ code: "REL_NO_ROOT", severity: "error", message: "the project has no root system" }] };
20
+ const byPath = new Map(p.instances.map((i) => [i.path, i]));
21
+
22
+ // ---- which instance roots which unit
23
+ const unitAt = new Map<string, string>([["", ROOT_UNIT]]);
24
+ const declared: { id: string; entry: string; kind: Unit["kind"]; instances: string[] }[] = [];
25
+ for (const entry of entries) {
26
+ if (entry === "" || entry === p.rootDef.id) {
27
+ diagnostics.push({ code: "REL_UNIT_ROOT", severity: "info", message: `release.units names the root (${entry || '""'}); the root system is always a unit` });
28
+ continue;
29
+ }
30
+ const inst = byPath.get(entry);
31
+ const instances = inst ? [inst.path] : p.instances.filter((i) => i.path !== "" && i.definition === entry).map((i) => i.path);
32
+ if (!instances.length) {
33
+ err(p.definitions.has(entry) ? `release.units names module ${entry}, which the system does not use` : `release.units names ${entry}, which is neither an instance path nor a module id in the system`);
34
+ continue;
35
+ }
36
+ const id = entry.split("/").join(".");
37
+ if (id === ROOT_UNIT || declared.some((d) => d.id === id)) {
38
+ err(`release.units: ${entry} gives the unit folder "${id}", which is taken`);
39
+ continue;
40
+ }
41
+ const taken = instances.filter((i) => unitAt.has(i));
42
+ if (taken.length) {
43
+ err(`release.units: ${taken.join(", ")} ${taken.length > 1 ? "are" : "is"} already in unit ${unitAt.get(taken[0])}; name each instance once (by path or by module id)`);
44
+ continue;
45
+ }
46
+ for (const i of instances) unitAt.set(i, id);
47
+ declared.push({ id, entry, kind: inst ? "instance" : "module", instances });
48
+ }
49
+
50
+ const ownerOf = (path: string): string => {
51
+ for (let at = parentOf(path); ; at = parentOf(at)) {
52
+ if (unitAt.has(at)) return unitAt.get(at)!;
53
+ if (at === "") return ROOT_UNIT;
54
+ }
55
+ };
56
+
57
+ // a unit named by path may not sit inside one build of a module unit built several times: the builds would differ
58
+ for (const d of declared.filter((x) => x.kind === "instance")) {
59
+ for (let at = parentOf(d.instances[0]); at !== ""; at = parentOf(at)) {
60
+ const outer = declared.find((x) => x.instances.includes(at));
61
+ if (outer?.kind === "module" && outer.instances.length > 1) {
62
+ err(`release.units: ${d.entry} is inside ${at}, one of ${outer.instances.length} builds of unit ${outer.id}; name it by module id (${byPath.get(d.instances[0])!.definition}) so every build is the same`);
63
+ break;
64
+ }
65
+ }
66
+ }
67
+
68
+ // ---- quantities through the tree
69
+ const total = (path: string): number => {
70
+ const inst = byPath.get(path);
71
+ if (!inst || path === "") return 1;
72
+ return inst.quantity * total(inst.parent ?? "");
73
+ };
74
+ /** Quantity of `path` per build of the unit rooted at `base` (base excluded). */
75
+ const within = (base: string, path: string): number => {
76
+ let q = 1;
77
+ for (let at = path; at !== base && at !== ""; at = parentOf(at)) q *= byPath.get(at)?.quantity ?? 1;
78
+ return q;
79
+ };
80
+
81
+ const make = (id: string, kind: Unit["kind"], entry: string, instances: string[]): Unit => {
82
+ const representative = instances[0];
83
+ const repInst = byPath.get(representative)!;
84
+ const def = p.lookup(repInst.definition)!;
85
+ const members: UnitMember[] = [];
86
+ const walk = (path: string) => {
87
+ for (const child of p.instances.filter((i: Instance) => i.parent === path && i.path !== "")) {
88
+ const sub = unitAt.get(child.path);
89
+ members.push({
90
+ path: relTo(representative, child.path),
91
+ instance: child.path,
92
+ parent: relTo(representative, path),
93
+ definition: child.definition,
94
+ name: child.name ?? p.lookup(child.definition)?.name ?? child.definition,
95
+ kind: child.kind,
96
+ quantity: within(representative, child.path),
97
+ ...(sub ? { unit: sub } : {}),
98
+ depth: relTo(representative, child.path).split("/").length - 1,
99
+ });
100
+ if (!sub) walk(child.path);
101
+ }
102
+ };
103
+ walk(representative);
104
+ const parents = kind === "root" ? [] : [...new Set(instances.map(ownerOf))];
105
+ return {
106
+ id,
107
+ folder: kind === "root" ? ROOT_UNIT : `units/${id}`,
108
+ kind,
109
+ entry,
110
+ name: kind === "root" ? (def.name ?? def.id) : kind === "instance" ? (repInst.name ?? def.name ?? def.id) : (def.name ?? def.id),
111
+ definition: def.id,
112
+ instances,
113
+ representative,
114
+ perProduct: kind === "root" ? 1 : instances.reduce((s, i) => s + total(i), 0),
115
+ parents,
116
+ members,
117
+ };
118
+ };
119
+
120
+ const units = [make(ROOT_UNIT, "root", "", [""]), ...declared.map((d) => make(d.id, d.kind, d.entry, d.instances))];
121
+ // order of work: a unit is built after every unit assembled into it
122
+ const level = new Map<string, number>();
123
+ const levelOf = (u: Unit): number => {
124
+ if (level.has(u.id)) return level.get(u.id)!;
125
+ const subs = units.filter((x) => x.parents.includes(u.id));
126
+ const l = subs.length ? 1 + Math.max(...subs.map(levelOf)) : 0;
127
+ level.set(u.id, l);
128
+ return l;
129
+ };
130
+ const order = units.map((u, i) => ({ u, i, l: levelOf(u) })).sort((a, b) => a.l - b.l || (a.u.kind === "root" ? 1 : 0) - (b.u.kind === "root" ? 1 : 0) || a.i - b.i);
131
+ return { units: order.map((x) => x.u), diagnostics };
132
+ }
133
+
134
+ /** The unit an instance path belongs to (its own unit when it roots one). */
135
+ export function unitOfInstance(units: Unit[], path: string): Unit | undefined {
136
+ for (let at = path; ; at = parentOf(at)) {
137
+ const u = units.find((x) => x.instances.includes(at));
138
+ if (u) return u;
139
+ if (at === "") return units.find((x) => x.kind === "root");
140
+ }
141
+ }
package/lib/util.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** Small shared helpers: digests, CSV, file import, paths. */
2
+ import { createHash } from "node:crypto";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ export type { ReleaseConfig } from "@protoboardai/core";
6
+
7
+ export const sha256 = (bytes: string | Buffer) => "sha256:" + createHash("sha256").update(bytes).digest("hex");
8
+ export const posix = (p: string) => p.split("\\").join("/");
9
+
10
+ /** Import a TypeScript file (the CLI runs under tsx), bypassing the module cache. */
11
+ export async function importTsFile(file: string): Promise<Record<string, unknown>> {
12
+ return (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)) as Record<string, unknown>;
13
+ }
14
+
15
+ /** One RFC 4180 field. */
16
+ export function csvField(v: unknown): string {
17
+ if (v === undefined || v === null) return "";
18
+ const s = String(v);
19
+ return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
20
+ }
21
+
22
+ /** CSV with a header row from `columns`; LF line ends, final newline. */
23
+ export function toCsv(columns: string[], rows: Record<string, unknown>[]): string {
24
+ return [columns.map(csvField).join(","), ...rows.map((r) => columns.map((c) => csvField(r[c])).join(","))].join("\n") + "\n";
25
+ }
26
+
27
+ /** Compare strings by code point (locale-independent, so sorting is the same on every machine). */
28
+ export const byCode = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
29
+
30
+ /** Format a number with up to `digits` decimals, trailing zeros removed. */
31
+ export const num = (v: number, digits = 2) => String(Number(v.toFixed(digits)));
package/ops/release.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { OperationFn } from "@protoboardai/core";
2
+ import { buildRelease } from "../lib/release.ts";
3
+
4
+ const flag = (v: string | undefined) => v !== undefined && !/^(false|no|0|)$/i.test(v);
5
+
6
+ /**
7
+ * release: refuse unless the project is clean (no check errors, nothing
8
+ * stale, committed), then write the Manufacturing Package to
9
+ * release/<version>/. Every file is a recorded output of this run.
10
+ */
11
+ export const release: OperationFn = async (ctx) => {
12
+ const r = await buildRelease(ctx.project, {
13
+ version: ctx.inputs.version || undefined,
14
+ allowDirty: flag(ctx.inputs["allow-dirty"]),
15
+ checkOnly: flag(ctx.inputs.check),
16
+ write: (path, content, description) => ctx.writeOutput(path, content, description),
17
+ log: ctx.log,
18
+ });
19
+ for (const d of r.diagnostics.filter((d) => d.severity !== "info")) ctx.log(`${d.severity.toUpperCase().padEnd(7)} ${d.code} ${d.message}`);
20
+ return { status: r.status, summary: r.summary, data: r };
21
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The release's own contributions to each section (package:<section>), from
3
+ * the model and the collected files. Other extensions declare the same
4
+ * operation ids to add to a section. `protoboard release` calls them once
5
+ * per unit with a SectionContext; run on their own they only say so.
6
+ */
7
+ import type { OperationResult } from "@protoboardai/core";
8
+ import { BASELINE } from "../lib/sections/baseline.ts";
9
+ import type { SectionContext, SectionFn } from "../lib/types.ts";
10
+
11
+ const section =
12
+ (id: string): SectionFn =>
13
+ (ctx) =>
14
+ "unit" in ctx && ctx.unit
15
+ ? BASELINE[id](ctx)
16
+ : ({ status: "failed", summary: `package:${id} contributes to the Manufacturing Package once per unit; run \`protoboard release\`` } as OperationResult as never);
17
+
18
+ export const spec = section("spec");
19
+ export const bom = section("bom");
20
+ export const workOrder = section("work-order");
21
+ export const assembly = section("assembly");
22
+ export const testPlan = section("test-plan");
23
+ export type { SectionContext };
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@protoboardai/ext-release",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Protoboard extension: the manufacturing package, from BOM and ordering list to assembly guide and test plan.",
5
+ "license": "Apache-2.0",
6
+ "author": "Delta Robotics, Inc.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Delta-Robotics-Inc/protoboard-stack.git",
10
+ "directory": "packages/ext-release"
11
+ },
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./lib/types.ts",
15
+ "./helpers": "./lib/helpers.ts",
16
+ "./testing": "./lib/testing.ts",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "protoboard-extension.json",
21
+ "README.md",
22
+ "lib",
23
+ "ops",
24
+ "skills",
25
+ "evals",
26
+ "LICENSE",
27
+ "NOTICE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@protoboardai/ext-docs": "^0.2.0",
37
+ "@protoboardai/ext-sourcing": "^0.2.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@protoboardai/core": "^0.2.0",
41
+ "@deltarobotics/uhd": ">=0.3.0"
42
+ },
43
+ "keywords": [
44
+ "protoboard",
45
+ "hardware",
46
+ "manufacturing",
47
+ "bom",
48
+ "release"
49
+ ],
50
+ "homepage": "https://protoboard.ai",
51
+ "bugs": {
52
+ "url": "https://github.com/Delta-Robotics-Inc/protoboard-stack/issues"
53
+ }
54
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "schema": "protoboard.extension/v0",
3
+ "id": "@protoboardai/ext-release",
4
+ "version": "0.2.1",
5
+ "description": "Releases: `protoboard release` refuses a project with check errors, stale outputs or uncommitted changes, then writes the Manufacturing Package (overview, per-unit spec sheet, BOM and ordering list, work order, assembly guide and test plan, manufacturing files, index.html) and a reproducible release manifest.",
6
+ "compatibility": {
7
+ "protoboard": "^0.2.0",
8
+ "uhd": ">=0.3.0"
9
+ },
10
+ "skills": [
11
+ {
12
+ "id": "release",
13
+ "summary": "Cut a release: choose the package units, clear every precondition (check errors, stale outputs, uncommitted changes), run `protoboard release`, and inspect the Manufacturing Package",
14
+ "path": "skills/release/SKILL.md"
15
+ }
16
+ ],
17
+ "tools": [
18
+ {
19
+ "id": "chrome",
20
+ "purpose": "A Chromium-family browser (Google Chrome, Chromium, Microsoft Edge or Brave), run headless: the package's documents are laid out, their figures drawn and the PDFs printed with it, through @protoboardai/ext-docs' renderer",
21
+ "probe": [
22
+ "protoboard:browser"
23
+ ],
24
+ "install": "install Google Chrome, Chromium, Microsoft Edge or Brave (found where each platform installs them), or set CHROME to a browser's executable"
25
+ }
26
+ ],
27
+ "operations": [
28
+ {
29
+ "id": "release",
30
+ "summary": "Write the Manufacturing Package to release/<version>/ (units from release.units; one folder per unit) with its manifest, after checking the project is clean",
31
+ "entry": "./ops/release.ts#release",
32
+ "effect": "export",
33
+ "requires": [
34
+ "chrome"
35
+ ],
36
+ "inputs": {
37
+ "version": {
38
+ "description": "release version (default: the root module's version)"
39
+ },
40
+ "allow-dirty": {
41
+ "description": "release from uncommitted changes (true/false); recorded in the manifest",
42
+ "default": "false"
43
+ },
44
+ "check": {
45
+ "description": "only run the preconditions and report (true/false); writes nothing",
46
+ "default": "false"
47
+ }
48
+ }
49
+ },
50
+ {
51
+ "id": "package:spec",
52
+ "summary": "Spec sheet: what the unit is, key figures, contents, interfaces (from the model)",
53
+ "entry": "./ops/sections.ts#spec",
54
+ "effect": "pure"
55
+ },
56
+ {
57
+ "id": "package:bom",
58
+ "summary": "Bill of materials: the unit's module breakdown (bom.csv); the root also gets the whole-product ordering list (ordering-list.csv)",
59
+ "entry": "./ops/sections.ts#bom",
60
+ "effect": "pure"
61
+ },
62
+ {
63
+ "id": "package:work-order",
64
+ "summary": "Work order: ordering checklist, fabrication orders per custom part, assembly operations from the model",
65
+ "entry": "./ops/sections.ts#workOrder",
66
+ "effect": "pure"
67
+ },
68
+ {
69
+ "id": "package:assembly",
70
+ "summary": "Assembly guide: kit and one step per joint the unit makes",
71
+ "entry": "./ops/sections.ts#assembly",
72
+ "effect": "pure"
73
+ },
74
+ {
75
+ "id": "package:test-plan",
76
+ "summary": "Test plan: first-article inspection, fits and joints, records to capture",
77
+ "entry": "./ops/sections.ts#testPlan",
78
+ "effect": "pure"
79
+ }
80
+ ]
81
+ }
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: release
3
+ description: Cut a release of a Protoboard project with `protoboard release` — choose the package units, clear every precondition (check errors, stale generated outputs, uncommitted changes), write the Manufacturing Package (overview, per-unit spec sheet, BOM and ordering list, work order, assembly guide, test plan, manufacturing files, index.html, manifest) and inspect it. Use when a design is ready to hand to purchasing, fabrication, assembly or test, or before physical tests that must trace to an identified revision.
4
+ ---
5
+
6
+ # Release
7
+
8
+ ## Purpose
9
+
10
+ A release is a fixed, reproducible snapshot of the project and the
11
+ Manufacturing Package made from it: everything needed to order, make,
12
+ assemble and check the product, laid out by unit. `protoboard release`
13
+ writes it to `release/<version>/`. It never orders, uploads or prints
14
+ anything. Design: `docs/specs/release-package-and-evals.md` in the stack
15
+ repository; the package's contracts are in this extension's README.
16
+
17
+ Use it at the end of build-a-project, and again for every revision a
18
+ fabricator, assembler or tester will see.
19
+
20
+ ## Inputs
21
+
22
+ | Input | Where |
23
+ | --- | --- |
24
+ | The model, parts, providers' standard outputs (`<module>/out/`) | the project |
25
+ | Operation records of every generated file | `operations/` |
26
+ | Package units, products to build, spare rules | `release` in `protoboard.config.ts`, or `release.config.ts` |
27
+ | Offers (offline only) | `parts/offers.json` (`protoboard parts offers`), `parts/evidence/<id>/sourcing.json` (ext-sourcing) |
28
+ | Version | `--version <v>`, default the root module's `version` |
29
+ | Assembly order, cameras, step notes | `docs/docspec.ts` (ext-docs); without one, steps follow the model's mates then connections |
30
+ | What the product is | `docs/product.json` (summary, features, functions, requirements): the overview's first page. Empty parts are a warning (`REL_PRODUCT_NOT_STATED`) and the page says they are not stated |
31
+ | Requirements and power budget | `docs/product.json` requirements, else the table in `docs/brief.md` (functional checks); `docs/power-budget.md` (named in bring-up) |
32
+ | Fab and print settings | each module's `circuit.config.ts` / `cad.config.ts` constraints (finish, copper, mask, material, wall) |
33
+ | Tools | Google Chrome (`$CHROME`), probed by `protoboard doctor`; Kode Mono from ext-docs; poppler (`pdftoppm`) to review pages |
34
+
35
+ ## Steps
36
+
37
+ ### 1. Choose the units
38
+
39
+ In `protoboard.config.ts`:
40
+
41
+ ```ts
42
+ release: {
43
+ units: ["stack", "quadcopter-5in-arm"], // instance paths or module ids
44
+ build: 1, // products to build (ordering quantities × build)
45
+ },
46
+ ```
47
+
48
+ An instance path makes that one instance a unit; a module id makes every
49
+ instance of it one unit built that many times. The root is always the
50
+ `system/` unit (final assembly). Everything else folds into the nearest
51
+ unit above it.
52
+
53
+ ### 2. Bring generated outputs up to date
54
+
55
+ For each module with a
56
+ `circuit.config.ts`, `cad.config.ts` or `firmware.config.ts`: `npx protoboard run
57
+ <capability>:generate --module <id>` then `:export`. Re-run other
58
+ generators whose files the package carries (`npx protoboard run
59
+ betaflight-cli`). Never edit generated files.
60
+
61
+ Order matters: an enclosure is designed around the boards and parts it
62
+ holds, so re-exporting a board (or changing a part it holds) can make the
63
+ enclosure stale. After any board export, regenerate and re-export every
64
+ enclosure or cad module around it (`cad:generate`, then `cad:export`),
65
+ then run `check` again; `ENCLOSURE_*_STALE` or `REL_STALE` naming the
66
+ enclosure means this step was skipped.
67
+
68
+ After any change to UHD itself (a stack developer working on
69
+ `@deltarobotics/uhd`), rebuild its `dist` (`npm run build` in the uhd
70
+ checkout) before trusting `check` or `release`: the stack reads the built
71
+ package, not the sources, so an unbuilt change is not in the result.
72
+
73
+ ### 3. Check
74
+
75
+ `npx protoboard check` must report 0 errors. Read every
76
+ warning: the manifest records them, and the reader of the package will
77
+ see them in the traceability appendix.
78
+
79
+ ### 4. Commit
80
+
81
+ `git status` must be clean outside `release/` and
82
+ `operations/`.
83
+
84
+ ### 5. Dry run
85
+
86
+ `npx protoboard release --check` lists every blocking
87
+ problem and writes nothing. Fix each (see Failure handling) and repeat.
88
+
89
+ ### 6. Release
90
+
91
+ `npx protoboard release [--version <v>]` (`--version <v>` is this
92
+ release's version, default the root module's; `npx protoboard --version`
93
+ alone still prints the CLI's). The output lists each document with its
94
+ page count; `--format json` gives the operation
95
+ record with `data.packageDigest` and `data.manifestDigest`.
96
+
97
+ ### 7. Inspect
98
+
99
+ (see Verification): render the pages and read the work
100
+ order, assembly guide and test plan of every unit as the person who will
101
+ use them, in build order: a sub-unit must not do work that final assembly
102
+ undoes or repeats (an enclosure unit ends at the printed parts with its
103
+ inserts set; O-rings, plungers and the lid are fitted where the contents go
104
+ in), and each bring-up row must be a measurement that can pass or fail on
105
+ this board (rails at their design values, the programming port the board
106
+ exports, a scan of each bus).
107
+
108
+ ## Decision points
109
+
110
+ | Question | Rule |
111
+ | --- | --- |
112
+ | Which modules are units? | A module built, tested or bought as a separate sub-assembly (a flight stack, a board, an arm built four times, an enclosure). Not a single purchased part. |
113
+ | Instance path or module id? | Module id when every instance is built the same way (four arms); instance path when only that one is a separate build. A unit named by path cannot sit inside one build of a module unit built several times. |
114
+ | `--allow-dirty`? | Only for a trial package nobody will build from. The manifest lists the uncommitted files. |
115
+ | Version | A new version for every change anyone downstream will see. Rebuilding an existing version replaces its folder; that is only right when nothing changed (the manifest digests show it). |
116
+ | Spares | The defaults (mechanical hardware 10 %, at least 2; board components 5 %, at least 2) unless the build calls for more: `release.spares` by part id, tag or category. |
117
+ | A sub-assembly that holds a part fitted last (propellers, a battery)? | Not a unit: a unit is built and tested before final assembly. Leave it folded into the root, where the doc spec fits the part last. A step that fits propellers follows the bench test by itself; mark any other such step `after: "test"` in `docs/docspec.ts`. |
118
+ | A module unit whose builds differ (an arm whose motor spins CW at two positions and CCW at two)? | The documents list the variant per build where a step depends on it. On `REL_BUILDS_DIFFER` no step does: name each instance as its own unit, or keep the part out of the unit. |
119
+ | A step title that reads wrong in a unit (a doc spec step split across units) | Split the step in `docs/docspec.ts` into one per unit (by link patterns), so each unit's guide and work order get their own title. |
120
+ | The test plan misses what only the project knows how to test (a display pattern, a charge current, a sleep current) | Add `release.checks` in protoboard.config.ts: `{ unit?, group?, phase?, check, subject, how, pass, record, trace }`, with the expected value cited in `trace`. They follow the generated bring-up (or functions) of that unit; bring-up checks are numbered power-phase first, then those that need firmware. Keep process rules out of the brief's requirement sections, or they become functional checks. |
121
+ | A functional check's method is guessed from the requirement's wording ("Measure, with the instrument the value calls for") | State how each requirement is shown: `how`, `pass` and `record` on each requirement of `docs/product.json`, or a requirements table in the brief with the columns ID, Requirement, How, Pass and Record (examples/quadcopter-5in/docs/brief.md). Each FN check takes them as written. |
122
+ | `REL_PRODUCT_NOT_STATED`: the first page says "The project has not stated …" | Fill `docs/product.json` from the brief (the system-compose skill, step 1). Do not write what the brief does not say; ask the person whose product it is. |
123
+ | "unknown" supplier, lead time or MOQ on the checklist | Record offers (`protoboard parts offers`, or ext-sourcing `sourcing.json`) before ordering; never type them into the package. |
124
+ | A fab or print default is wrong for this build (finish, copper, material) | State it in the module's config constraints and regenerate; the order says which values are defaults. |
125
+
126
+ ## Outputs
127
+
128
+ `release/<version>/`: `00-overview.pdf`, `index.html`, `manifest.json`
129
+ (`protoboard.release/v0`), `system/` and `units/<unit>/` with
130
+ `spec-sheet.pdf`, `bom.pdf`, `bom.csv`, `work-order.pdf`,
131
+ `assembly-guide.pdf`, `test-plan.pdf` and `files/` (boards, mechanical,
132
+ harnesses, firmware, model), and `system/ordering-list.csv`. An operation
133
+ record in `operations/` lists every file with its digest.
134
+
135
+ ## Verification
136
+
137
+ 1. Rebuild in a clean clone (or rerun after deleting `release/`) and compare
138
+ `manifest.json`: the same inputs give the same bytes.
139
+ 2. Open `index.html`: every unit has five sections and its files; the
140
+ traceability page names the source commit, model digest, part revisions
141
+ and provider outputs.
142
+ 3. Read `system/ordering-list.csv`: no "no offer recorded" line you did not
143
+ expect; included parts at order 0; custom parts as `fabricate`.
144
+ 4. Page through the PDFs: no `REL_PAGE_OVERFLOW` warning in the output.
145
+ `pdftoppm -r 60 -png release/<v>/system/work-order.pdf /tmp/wo` renders
146
+ pages to look at; check that no heading is left alone at a page foot and
147
+ no figure is blank.
148
+ 5. Work order: the bench test (and any configuration, e.g. ext-drone's
149
+ flash and load) comes before every operation that must follow it
150
+ (propellers, the battery strapped on) and a final check ends the root's
151
+ work; every operation names an inspection point and a test id that
152
+ exists in the same unit's test plan; the kitting table covers every part
153
+ of the unit's bill of materials; each made part has its provider's order
154
+ details (board, cut, print, cables).
155
+ 6. Assembly guide: every step has a figure where the model has CAD for its
156
+ parts, letters on the figure match the parts strip, and the coverage
157
+ table lists every mate and connection the unit makes.
158
+ 7. Test plan: bring-up rails and addresses match the parts' datasheets; the
159
+ functional checks cover every requirement of `docs/brief.md`. It is basic
160
+ (v0.1): refine methods and tolerances before a qualification run.
161
+
162
+ ## Failure handling
163
+
164
+ | Diagnostic | Meaning | Do |
165
+ | --- | --- | --- |
166
+ | `REL_CHECK_ERRORS` | `protoboard check` has errors | Fix them; `protoboard check` lists each with its source line. |
167
+ | `REL_STALE` (`<path> was changed after <op> wrote it`) | A generated file was edited | Re-run the operation; never edit generated files. |
168
+ | `REL_STALE` (`… is not from the last export` / `… is missing`) | `out/` differs from the last export's record | Re-run `<capability>:export --module <id>`. |
169
+ | `REL_STALE` (`<config> changed since the last export`, `<module> changed since its last export`) | The design moved on | `generate`, then `export`. |
170
+ | `REL_STALE` (`<CODE>_STALE …`) | A provider's check reports its design out of date | Do what the provider's message says. |
171
+ | `REL_STALE` on an enclosure right after a board export | The enclosure was designed around the board's previous outputs | `cad:generate`, then `cad:export` for the enclosure. |
172
+ | `REL_STALE` from a gitignored native folder (a provider's `build/`, older than its operation record) | A native build folder that git does not track is older than the record of the last export, so the check sees the design as behind | Regenerate the native design and export again (`cad:generate`, then `cad:export --module <id>`); do not edit or touch the folder by hand. |
173
+ | `check` or `release` disagrees with a UHD change you just made | The stack reads UHD's built `dist`, not its sources | `npm run build` in the uhd checkout, then run `check` again. |
174
+ | `REL_FRESHNESS_UNKNOWN` | A provider's check was skipped (tool missing) | Install the tool (`protoboard doctor`, `protoboard tools install <runtime>`). |
175
+ | `REL_NOT_EXPORTED` | A bound module has no `out/` | Run its `export`. |
176
+ | `REL_DIRTY` | Uncommitted changes | Commit, or `--allow-dirty` for a trial. |
177
+ | `REL_UNIT_INVALID` | A `release.units` entry is unknown, duplicated or ambiguous | Use an instance path from `protoboard inspect` or a module id the system uses. |
178
+ | `REL_SECTION_FAILED`, `REL_SECTION_FILE`, `DRONE_CLI_STALE`, … | An extension's section failed or refused | Read the message; `DRONE_CLI_STALE`: `protoboard run betaflight-cli`. |
179
+ | `REL_SECTION_FAILED` … `figure "…" was already rendered from another spec` | Two contributions use one figure name | A contributor's bug: report it with the extension's name. |
180
+ | A step without a figure, or a "CAD file not available" kit item | The part's body file is not in the checkout (vendor CAD not redistributed) | Expected for such parts; the step's table and actions still hold. |
181
+ | Bring-up lists no rails or no bus scan you expected | The parts state no voltage on their outputs, or no `i2c_address` | Add the facts to the part (with a source) and release again. |
182
+ | `REL_AFTER_TEST_IN_SUBUNIT` (warning) | A unit built before final assembly fits something that must follow the product's bench test (propellers) | Do not make that module a unit; the root then fits it after the test. |
183
+ | `REL_BUILDS_DIFFER` (warning) | A module unit's builds differ in what their parent sets, and no step says which build gets what | Name each instance as its own unit, or check every build by hand. |
184
+ | Operation refused: `required tool(s) unavailable: chrome` | No Chrome | Install it or set `CHROME`. |
185
+
186
+ ## After the release
187
+
188
+ Physical units and test results name the release version and the unit
189
+ (test-data `conditions`: "Release", "Unit"; **test-and-measure**). A problem
190
+ found later is a change in the next version; a released folder is never
191
+ edited.