@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/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +290 -2
- package/evals/release-after-hand-edit/case.json +59 -0
- package/lib/bom.ts +444 -0
- package/lib/collect.ts +100 -0
- package/lib/config.ts +45 -0
- package/lib/cutlist.ts +70 -0
- package/lib/documents.ts +287 -0
- package/lib/glb.ts +85 -0
- package/lib/helpers.ts +49 -0
- package/lib/html.ts +126 -0
- package/lib/manifest.ts +159 -0
- package/lib/offers.ts +123 -0
- package/lib/preconditions.ts +141 -0
- package/lib/product.ts +64 -0
- package/lib/release.ts +279 -0
- package/lib/render.ts +203 -0
- package/lib/sections/baseline.ts +164 -0
- package/lib/sections/bom.ts +101 -0
- package/lib/sections/model.ts +78 -0
- package/lib/sections/testplan.ts +76 -0
- package/lib/sections/tests.ts +630 -0
- package/lib/sections/workorder.ts +369 -0
- package/lib/testing.ts +43 -0
- package/lib/types.ts +251 -0
- package/lib/units.ts +141 -0
- package/lib/util.ts +31 -0
- package/ops/release.ts +21 -0
- package/ops/sections.ts +23 -0
- package/package.json +52 -4
- package/protoboard-extension.json +81 -0
- package/skills/release/SKILL.md +191 -0
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
|
+
};
|
package/ops/sections.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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.
|