copperhead 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +13 -5
  3. package/dist/agent/filetools.js +24 -1
  4. package/dist/agent/filetools.js.map +1 -1
  5. package/dist/agent/ledger.js +24 -0
  6. package/dist/agent/ledger.js.map +1 -1
  7. package/dist/agent/loop.js +67 -62
  8. package/dist/agent/loop.js.map +1 -1
  9. package/dist/agent/prompts.js +4 -3
  10. package/dist/agent/prompts.js.map +1 -1
  11. package/dist/agent/providers/openai.js +28 -6
  12. package/dist/agent/providers/openai.js.map +1 -1
  13. package/dist/agent/providers/tool-protocol.js +21 -0
  14. package/dist/agent/providers/tool-protocol.js.map +1 -1
  15. package/dist/agent/recovery.js +95 -1
  16. package/dist/agent/recovery.js.map +1 -1
  17. package/dist/agent/response-cache.js +18 -2
  18. package/dist/agent/response-cache.js.map +1 -1
  19. package/dist/agent/tools.js +185 -1
  20. package/dist/agent/tools.js.map +1 -1
  21. package/dist/agent/transcript.js +2 -0
  22. package/dist/agent/transcript.js.map +1 -1
  23. package/dist/cli.js +77 -2
  24. package/dist/cli.js.map +1 -1
  25. package/dist/commands/check.js +33 -1
  26. package/dist/commands/check.js.map +1 -1
  27. package/dist/commands/create.js +282 -26
  28. package/dist/commands/create.js.map +1 -1
  29. package/dist/commands/doctor.js +211 -11
  30. package/dist/commands/doctor.js.map +1 -1
  31. package/dist/config.js +61 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/kicad/bootstrap.js +24 -3
  34. package/dist/kicad/bootstrap.js.map +1 -1
  35. package/dist/kicad/cli.js +7 -26
  36. package/dist/kicad/cli.js.map +1 -1
  37. package/dist/kicad/dossier.js +207 -0
  38. package/dist/kicad/dossier.js.map +1 -0
  39. package/dist/kicad/draft/draft.js +132 -0
  40. package/dist/kicad/draft/draft.js.map +1 -0
  41. package/dist/kicad/draft/engine.js +2389 -0
  42. package/dist/kicad/draft/engine.js.map +1 -0
  43. package/dist/kicad/draft/ir.js +368 -0
  44. package/dist/kicad/draft/ir.js.map +1 -0
  45. package/dist/kicad/draft/symsource.js +490 -0
  46. package/dist/kicad/draft/symsource.js.map +1 -0
  47. package/dist/kicad/emit.js +181 -0
  48. package/dist/kicad/emit.js.map +1 -0
  49. package/dist/kicad/fab.js +13 -0
  50. package/dist/kicad/fab.js.map +1 -1
  51. package/dist/kicad/legibility.js +561 -0
  52. package/dist/kicad/legibility.js.map +1 -0
  53. package/dist/kicad/score.js +261 -0
  54. package/dist/kicad/score.js.map +1 -0
  55. package/dist/kicad/sexp.js +262 -10
  56. package/dist/kicad/sexp.js.map +1 -1
  57. package/dist/kicad/symlib.js +346 -16
  58. package/dist/kicad/symlib.js.map +1 -1
  59. package/dist/memory/bom-table.js +108 -34
  60. package/dist/memory/bom-table.js.map +1 -1
  61. package/dist/memory/scaffold.js +6 -0
  62. package/dist/memory/scaffold.js.map +1 -1
  63. package/dist/openspec/cli.js +2 -1
  64. package/dist/openspec/cli.js.map +1 -1
  65. package/dist/util/preflight.js +17 -0
  66. package/dist/util/preflight.js.map +1 -1
  67. package/dist/util/redact.js +12 -2
  68. package/dist/util/redact.js.map +1 -1
  69. package/package.json +9 -7
  70. package/src/agent/filetools.ts +26 -1
  71. package/src/agent/ledger.ts +24 -0
  72. package/src/agent/loop.ts +88 -65
  73. package/src/agent/prompts.ts +4 -3
  74. package/src/agent/providers/openai.ts +38 -4
  75. package/src/agent/providers/tool-protocol.ts +22 -0
  76. package/src/agent/recovery.ts +94 -1
  77. package/src/agent/response-cache.ts +17 -1
  78. package/src/agent/tools.ts +189 -1
  79. package/src/agent/transcript.ts +6 -0
  80. package/src/cli.ts +73 -2
  81. package/src/commands/check.ts +51 -1
  82. package/src/commands/create.ts +278 -22
  83. package/src/commands/doctor.ts +219 -12
  84. package/src/config.ts +107 -2
  85. package/src/kicad/bootstrap.ts +24 -3
  86. package/src/kicad/cli.ts +6 -19
  87. package/src/kicad/dossier.ts +217 -0
  88. package/src/kicad/draft/draft.ts +171 -0
  89. package/src/kicad/draft/engine.ts +2466 -0
  90. package/src/kicad/draft/ir.ts +416 -0
  91. package/src/kicad/draft/symsource.ts +535 -0
  92. package/src/kicad/emit.ts +236 -0
  93. package/src/kicad/fab.ts +15 -0
  94. package/src/kicad/legibility.ts +646 -0
  95. package/src/kicad/score.ts +323 -0
  96. package/src/kicad/sexp.ts +339 -10
  97. package/src/kicad/symlib.ts +364 -18
  98. package/src/memory/bom-table.ts +119 -31
  99. package/src/memory/scaffold.ts +6 -0
  100. package/src/openspec/cli.ts +3 -2
  101. package/src/util/preflight.ts +18 -0
  102. package/src/util/redact.ts +12 -2
  103. package/dist/memory/synap.js +0 -152
  104. package/dist/memory/synap.js.map +0 -1
  105. package/src/memory/synap.ts +0 -217
@@ -0,0 +1,236 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /**
4
+ * Deterministic KiCad schematic emitter (design D3/D4). A string-template
5
+ * module, NOT a serializing parser: the read-only parser in sexp.ts stays
6
+ * read-only, and this module builds canonical text directly from a placement
7
+ * model. Identical models yield byte-identical files: every UUID derives from
8
+ * a stable semantic path (UUIDv5), elements are emitted in a canonical sort
9
+ * order, and numbers use KiCad's trailing-zero-trimmed formatting.
10
+ */
11
+
12
+ /** Pinned format target: matches the scaffold (bootstrap.ts) and kicad-cli 8/9. */
13
+ export const EMIT_VERSION = '20231120';
14
+ export const EMIT_GENERATOR = 'copperhead-draft';
15
+ export const EMIT_GENERATOR_VERSION = '8.0';
16
+
17
+ /** Fixed namespace for copperhead-derived UUIDs (an arbitrary but constant v4). */
18
+ const COPPERHEAD_NAMESPACE = 'a1b2c3d4-0000-4000-8000-c0bbe4ead001';
19
+
20
+ /** RFC-4122 UUIDv5 (SHA-1, name-based). Same inputs, same UUID, any machine. */
21
+ export function uuidv5(name: string, namespace: string = COPPERHEAD_NAMESPACE): string {
22
+ const ns = Buffer.from(namespace.replace(/-/g, ''), 'hex');
23
+ const hash = createHash('sha1').update(Buffer.concat([ns, Buffer.from(name, 'utf8')])).digest();
24
+ hash[6] = (hash[6]! & 0x0f) | 0x50;
25
+ hash[8] = (hash[8]! & 0x3f) | 0x80;
26
+ const hex = hash.subarray(0, 16).toString('hex');
27
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
28
+ }
29
+
30
+ /** KiCad number formatting: up to 4 decimals, trailing zeros (and dot) trimmed. */
31
+ export const knum = (n: number): string => {
32
+ const r = Math.round(n * 10000) / 10000;
33
+ return Object.is(r, -0) ? '0' : String(r);
34
+ };
35
+
36
+ const q = (s: string): string => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
37
+
38
+ export interface EmitSymbol {
39
+ ref: string;
40
+ libId: string;
41
+ value: string;
42
+ footprint: string;
43
+ at: { x: number; y: number; rot: number };
44
+ /** Reference/Value text anchors (absolute). Hidden properties reuse the origin. */
45
+ refAt: { x: number; y: number };
46
+ valueAt: { x: number; y: number };
47
+ /** Power-port symbols hide their reference text. */
48
+ hideRef?: boolean;
49
+ hideValue?: boolean;
50
+ /** Pin numbers, for the per-pin uuid entries KiCad requires. For a
51
+ * multi-unit instance these are that UNIT's pins only. */
52
+ pinNumbers: string[];
53
+ /** KiCad unit number for one placed unit of a multi-unit symbol. Several
54
+ * entries may share a `ref`, one per unit; omitted means unit 1. */
55
+ unit?: number;
56
+ }
57
+
58
+ export interface PlacementModel {
59
+ projectName: string;
60
+ paper: string;
61
+ title: { title: string; date: string; rev: string };
62
+ /** Verbatim `(symbol "Name" …)` source blocks keyed by target lib_id (design D3). */
63
+ libSymbols: { libId: string; sourceText: string }[];
64
+ symbols: EmitSymbol[];
65
+ wires: { x1: number; y1: number; x2: number; y2: number; net: string; index: number }[];
66
+ junctions: { x: number; y: number }[];
67
+ labels: { name: string; x: number; y: number; rot: number }[];
68
+ noConnects: { x: number; y: number }[];
69
+ rectangles: { x1: number; y1: number; x2: number; y2: number; stroke: 'solid' | 'dash'; name: string }[];
70
+ captions: { text: string; x: number; y: number; name: string }[];
71
+ }
72
+
73
+ /**
74
+ * Rename a verbatim library `(symbol "Name" …)` block to its embedded lib_id.
75
+ * Identifier atoms only; every other body byte stays exactly as vendored.
76
+ *
77
+ * The unit children (`Name_<unit>_<style>`) are renamed with the parent:
78
+ * KiCad's schematic loader requires the child prefix to match the parent
79
+ * name, so a derived symbol emitted under its own lib_id with the base's
80
+ * children (`Regulator_Linear:AMS1117-3.3` wrapping `AP1117-15_0_1`) fails
81
+ * to load outright. Children carry the BARE name, no library nickname,
82
+ * matching how non-derived blocks already emit.
83
+ */
84
+ export function renameSymbolBlock(sourceText: string, libId: string): string {
85
+ const oldName = /^\s*\(symbol\s+"([^"]+)"/.exec(sourceText)?.[1];
86
+ const bare = libId.includes(':') ? libId.slice(libId.indexOf(':') + 1) : libId;
87
+ const escaped = libId.replace(/\$/g, '$$$$');
88
+ let out = sourceText.replace(/^(\s*\(symbol\s+")[^"]+(")/, `$1${escaped}$2`);
89
+ if (oldName && oldName !== bare) {
90
+ const oldEsc = oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
91
+ out = out.replace(
92
+ new RegExp(`\\(symbol "${oldEsc}_(\\d+_\\d+)"`, 'g'),
93
+ `(symbol "${bare.replace(/\$/g, '$$$$')}_$1"`,
94
+ );
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** Re-indent a vendored block so lib_symbols nests consistently. Structural
100
+ * whitespace only; token bytes are untouched. */
101
+ function indentBlock(block: string, indent: string): string {
102
+ const lines = block.split('\n');
103
+ const first = lines.find((l) => l.trim());
104
+ const strip = first ? first.match(/^\s*/)![0].length : 0;
105
+ return lines
106
+ .map((l) => (l.trim() ? indent + (l.slice(0, strip).trim() === '' ? l.slice(strip) : l.trimStart()) : ''))
107
+ .join('\n');
108
+ }
109
+
110
+ export function emitSchematic(model: PlacementModel): string {
111
+ const ns = uuidv5(`project/${model.projectName}`);
112
+ const id = (path: string): string => uuidv5(path, ns);
113
+ const rootUuid = id('sheet/root');
114
+ const L: string[] = [];
115
+ L.push('(kicad_sch');
116
+ L.push(`\t(version ${EMIT_VERSION})`);
117
+ L.push(`\t(generator ${q(EMIT_GENERATOR)})`);
118
+ L.push(`\t(generator_version ${q(EMIT_GENERATOR_VERSION)})`);
119
+ L.push(`\t(uuid ${q(rootUuid)})`);
120
+ L.push(`\t(paper ${q(model.paper)})`);
121
+ L.push('\t(title_block');
122
+ L.push(`\t\t(title ${q(model.title.title)})`);
123
+ L.push(`\t\t(date ${q(model.title.date)})`);
124
+ L.push(`\t\t(rev ${q(model.title.rev)})`);
125
+ L.push('\t)');
126
+
127
+ const libs = [...model.libSymbols].sort((a, b) => a.libId.localeCompare(b.libId));
128
+ if (libs.length) {
129
+ L.push('\t(lib_symbols');
130
+ for (const lib of libs) L.push(indentBlock(renameSymbolBlock(lib.sourceText, lib.libId), '\t\t'));
131
+ L.push('\t)');
132
+ } else {
133
+ L.push('\t(lib_symbols)');
134
+ }
135
+
136
+ const juncs = [...model.junctions].sort((a, b) => a.x - b.x || a.y - b.y);
137
+ for (const j of juncs) {
138
+ L.push(`\t(junction (at ${knum(j.x)} ${knum(j.y)})`);
139
+ L.push('\t\t(diameter 0)');
140
+ L.push('\t\t(color 0 0 0 0)');
141
+ L.push(`\t\t(uuid ${q(id(`junction/${knum(j.x)},${knum(j.y)}`))})`);
142
+ L.push('\t)');
143
+ }
144
+
145
+ const ncs = [...model.noConnects].sort((a, b) => a.x - b.x || a.y - b.y);
146
+ for (const n of ncs) {
147
+ L.push(`\t(no_connect (at ${knum(n.x)} ${knum(n.y)}) (uuid ${q(id(`no_connect/${knum(n.x)},${knum(n.y)}`))}))`);
148
+ }
149
+
150
+ const wires = [...model.wires].sort((a, b) => a.net.localeCompare(b.net) || a.index - b.index);
151
+ for (const w of wires) {
152
+ L.push('\t(wire');
153
+ L.push(`\t\t(pts (xy ${knum(w.x1)} ${knum(w.y1)}) (xy ${knum(w.x2)} ${knum(w.y2)}))`);
154
+ L.push('\t\t(stroke (width 0) (type default))');
155
+ L.push(`\t\t(uuid ${q(id(`wire/${w.net}/${w.index}`))})`);
156
+ L.push('\t)');
157
+ }
158
+
159
+ const rects = [...model.rectangles].sort((a, b) => a.name.localeCompare(b.name));
160
+ for (const r of rects) {
161
+ L.push(`\t(rectangle (start ${knum(r.x1)} ${knum(r.y1)}) (end ${knum(r.x2)} ${knum(r.y2)})`);
162
+ L.push(`\t\t(stroke (width 0.152) (type ${r.stroke}))`);
163
+ L.push('\t\t(fill (type none))');
164
+ L.push(`\t\t(uuid ${q(id(`rect/${r.name}`))})`);
165
+ L.push('\t)');
166
+ }
167
+
168
+ const caps = [...model.captions].sort((a, b) => a.name.localeCompare(b.name));
169
+ for (const c of caps) {
170
+ L.push(`\t(text ${q(c.text)} (at ${knum(c.x)} ${knum(c.y)} 0)`);
171
+ L.push('\t\t(effects (font (size 2 2)) (justify left top))');
172
+ L.push(`\t\t(uuid ${q(id(`caption/${c.name}`))})`);
173
+ L.push('\t)');
174
+ }
175
+
176
+ const labels = [...model.labels].sort((a, b) => a.name.localeCompare(b.name) || a.x - b.x || a.y - b.y);
177
+ for (const lb of labels) {
178
+ const justify = lb.rot === 180 || lb.rot === 270 ? 'right bottom' : 'left bottom';
179
+ const drawRot = lb.rot === 180 ? 0 : lb.rot === 270 ? 90 : lb.rot;
180
+ L.push(`\t(label ${q(lb.name)} (at ${knum(lb.x)} ${knum(lb.y)} ${knum(drawRot)})`);
181
+ L.push(`\t\t(effects (font (size 1.27 1.27)) (justify ${justify}))`);
182
+ L.push(`\t\t(uuid ${q(id(`label/${lb.name}/${knum(lb.x)},${knum(lb.y)}`))})`);
183
+ L.push('\t)');
184
+ }
185
+
186
+ const syms = [...model.symbols].sort(
187
+ (a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }) || (a.unit ?? 1) - (b.unit ?? 1),
188
+ );
189
+ for (const s of syms) {
190
+ const unit = s.unit ?? 1;
191
+ // unit 1 keeps the historical uuid path so single-unit boards stay
192
+ // byte-identical; further units get their own stable path
193
+ const sid = id(unit === 1 ? `symbol/${s.ref}` : `symbol/${s.ref}/unit/${unit}`);
194
+ const hideRef = s.hideRef ? ' hide' : '';
195
+ const hideValue = s.hideValue ? ' hide' : '';
196
+ L.push('\t(symbol');
197
+ L.push(`\t\t(lib_id ${q(s.libId)})`);
198
+ L.push(`\t\t(at ${knum(s.at.x)} ${knum(s.at.y)} ${knum(s.at.rot)})`);
199
+ L.push(`\t\t(unit ${unit})`);
200
+ L.push('\t\t(exclude_from_sim no)');
201
+ L.push('\t\t(in_bom yes)');
202
+ L.push('\t\t(on_board yes)');
203
+ L.push('\t\t(dnp no)');
204
+ L.push(`\t\t(uuid ${q(sid)})`);
205
+ L.push(`\t\t(property "Reference" ${q(s.ref)} (at ${knum(s.refAt.x)} ${knum(s.refAt.y)} 0)`);
206
+ L.push(`\t\t\t(effects (font (size 1.27 1.27))${hideRef})`);
207
+ L.push('\t\t)');
208
+ L.push(`\t\t(property "Value" ${q(s.value)} (at ${knum(s.valueAt.x)} ${knum(s.valueAt.y)} 0)`);
209
+ L.push(`\t\t\t(effects (font (size 1.27 1.27))${hideValue})`);
210
+ L.push('\t\t)');
211
+ L.push(`\t\t(property "Footprint" ${q(s.footprint)} (at ${knum(s.at.x)} ${knum(s.at.y)} 0)`);
212
+ L.push('\t\t\t(effects (font (size 1.27 1.27)) hide)');
213
+ L.push('\t\t)');
214
+ L.push(`\t\t(property "Datasheet" "~" (at ${knum(s.at.x)} ${knum(s.at.y)} 0)`);
215
+ L.push('\t\t\t(effects (font (size 1.27 1.27)) hide)');
216
+ L.push('\t\t)');
217
+ for (const pin of s.pinNumbers) {
218
+ // units ≥ 2 scope their pin uuids: a common (unit-0) pin appears on
219
+ // every placed unit, and identical paths would mint duplicate uuids
220
+ const pinPath = unit === 1 ? `symbol/${s.ref}/pin/${pin}` : `symbol/${s.ref}/unit/${unit}/pin/${pin}`;
221
+ L.push(`\t\t(pin ${q(pin)} (uuid ${q(id(pinPath))}))`);
222
+ }
223
+ L.push('\t\t(instances');
224
+ L.push(`\t\t\t(project ${q(model.projectName)}`);
225
+ L.push(`\t\t\t\t(path ${q('/' + rootUuid)} (reference ${q(s.ref)}) (unit ${unit}))`);
226
+ L.push('\t\t\t)');
227
+ L.push('\t\t)');
228
+ L.push('\t)');
229
+ }
230
+
231
+ L.push('\t(sheet_instances');
232
+ L.push('\t\t(path "/" (page "1"))');
233
+ L.push('\t)');
234
+ L.push(')');
235
+ return L.join('\n') + '\n';
236
+ }
package/src/kicad/fab.ts CHANGED
@@ -20,6 +20,21 @@ export const DRAFT_QUALITY_HEADING = '## Draft quality';
20
20
  /** Config marker: repos produced by `copperhead create` set `origin` to `"create"`. */
21
21
  export const CREATE_ORIGIN = 'create';
22
22
 
23
+ /** Generator stamp carried by every schematic copperhead authors (the
24
+ * bootstrap scaffold and every emitter draft). */
25
+ export const DRAFT_GENERATOR_MARKER = '(generator "copperhead-draft")';
26
+
27
+ /**
28
+ * Is the schematic still copperhead-authored? True while the file carries the
29
+ * generator stamp. A human taking the sheet over in KiCad re-saves it with
30
+ * KiCad's own generator string, which is the deliberate one-way door out of
31
+ * drafting mode: the edit guard lifts and the legibility finish gate stops
32
+ * applying, because the drawing is no longer the engine's to defend.
33
+ */
34
+ export function isEngineAuthoredSchematic(text: string): boolean {
35
+ return text.slice(0, 400).includes(DRAFT_GENERATOR_MARKER);
36
+ }
37
+
23
38
  /**
24
39
  * True when `.copperhead/config.json` marks the repo as create-produced.
25
40
  * Accepts the raw parsed object (or a loaded config that retained `origin`).