copperhead 0.9.0 → 0.11.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.
- package/NOTICE +1 -1
- package/README.md +47 -8
- package/dist/agent/context.js +2 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/dock-renderer.js +2 -2
- package/dist/agent/dock-renderer.js.map +1 -1
- package/dist/agent/envelope.js +105 -0
- package/dist/agent/envelope.js.map +1 -0
- package/dist/agent/filetools.js +24 -1
- package/dist/agent/filetools.js.map +1 -1
- package/dist/agent/ledger.js +24 -0
- package/dist/agent/ledger.js.map +1 -1
- package/dist/agent/loop.js +63 -72
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +4 -3
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/claude-code.js +17 -1
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/codex.js +84 -39
- package/dist/agent/providers/codex.js.map +1 -1
- package/dist/agent/providers/tool-protocol.js +21 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -1
- package/dist/agent/recovery.js +186 -15
- package/dist/agent/recovery.js.map +1 -1
- package/dist/agent/registry.js +49 -0
- package/dist/agent/registry.js.map +1 -0
- package/dist/agent/render.js +2 -2
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/theme.js +10 -5
- package/dist/agent/theme.js.map +1 -1
- package/dist/agent/tools.js +100 -586
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js +2 -0
- package/dist/agent/transcript.js.map +1 -1
- package/dist/capabilities/define.js +35 -0
- package/dist/capabilities/define.js.map +1 -0
- package/dist/capabilities/handlers.js +744 -0
- package/dist/capabilities/handlers.js.map +1 -0
- package/dist/capabilities/helpers.js +39 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/index.js +50 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/skills/generate-report.js +23 -0
- package/dist/capabilities/skills/generate-report.js.map +1 -0
- package/dist/cli.js +158 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.js +33 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/create.js +181 -26
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +83 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/skill.js +109 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/sync.js +3 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/config.js +19 -6
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +24 -3
- package/dist/kicad/bootstrap.js.map +1 -1
- package/dist/kicad/cli.js +106 -18
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/dossier.js +207 -0
- package/dist/kicad/dossier.js.map +1 -0
- package/dist/kicad/draft/draft.js +135 -0
- package/dist/kicad/draft/draft.js.map +1 -0
- package/dist/kicad/draft/engine.js +5310 -0
- package/dist/kicad/draft/engine.js.map +1 -0
- package/dist/kicad/draft/ir.js +368 -0
- package/dist/kicad/draft/ir.js.map +1 -0
- package/dist/kicad/draft/symsource.js +504 -0
- package/dist/kicad/draft/symsource.js.map +1 -0
- package/dist/kicad/emit.js +220 -0
- package/dist/kicad/emit.js.map +1 -0
- package/dist/kicad/fab.js +13 -0
- package/dist/kicad/fab.js.map +1 -1
- package/dist/kicad/legibility.js +608 -0
- package/dist/kicad/legibility.js.map +1 -0
- package/dist/kicad/score.js +431 -0
- package/dist/kicad/score.js.map +1 -0
- package/dist/kicad/sexp.js +265 -6
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/kicad/symlib.js +346 -16
- package/dist/kicad/symlib.js.map +1 -1
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/bom-table.js +75 -39
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/scaffold.js +14 -1
- package/dist/memory/scaffold.js.map +1 -1
- package/dist/util/redact.js +6 -0
- package/dist/util/redact.js.map +1 -1
- package/package.json +13 -8
- package/src/agent/context.ts +35 -0
- package/src/agent/dock-renderer.ts +3 -2
- package/src/agent/envelope.ts +124 -0
- package/src/agent/filetools.ts +26 -1
- package/src/agent/ledger.ts +24 -0
- package/src/agent/loop.ts +73 -78
- package/src/agent/prompts.ts +4 -3
- package/src/agent/providers/claude-code.ts +22 -1
- package/src/agent/providers/codex.ts +91 -42
- package/src/agent/providers/tool-protocol.ts +22 -0
- package/src/agent/recovery.ts +183 -13
- package/src/agent/registry.ts +58 -0
- package/src/agent/render.ts +4 -3
- package/src/agent/theme.ts +15 -5
- package/src/agent/tools.ts +124 -628
- package/src/agent/transcript.ts +6 -0
- package/src/agent/types.ts +10 -5
- package/src/capabilities/define.ts +88 -0
- package/src/capabilities/handlers.ts +769 -0
- package/src/capabilities/helpers.ts +37 -0
- package/src/capabilities/index.ts +53 -0
- package/src/capabilities/skills/generate-report.ts +25 -0
- package/src/cli.ts +154 -0
- package/src/commands/check.ts +51 -1
- package/src/commands/create.ts +183 -21
- package/src/commands/doctor.ts +85 -6
- package/src/commands/skill.ts +127 -0
- package/src/commands/sync.ts +5 -3
- package/src/config.ts +56 -8
- package/src/kicad/bootstrap.ts +24 -3
- package/src/kicad/cli.ts +129 -18
- package/src/kicad/dossier.ts +217 -0
- package/src/kicad/draft/draft.ts +173 -0
- package/src/kicad/draft/engine.ts +5274 -0
- package/src/kicad/draft/ir.ts +416 -0
- package/src/kicad/draft/symsource.ts +549 -0
- package/src/kicad/emit.ts +300 -0
- package/src/kicad/fab.ts +15 -0
- package/src/kicad/legibility.ts +695 -0
- package/src/kicad/score.ts +502 -0
- package/src/kicad/sexp.ts +346 -6
- package/src/kicad/symlib.ts +364 -18
- package/src/mcp/server.ts +560 -0
- package/src/memory/bom-table.ts +85 -38
- package/src/memory/scaffold.ts +14 -1
- package/src/util/redact.ts +6 -0
- package/dist/memory/synap.js +0 -152
- package/dist/memory/synap.js.map +0 -1
- package/src/memory/synap.ts +0 -217
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isKicadFile } from '../util/paths.js';
|
|
2
|
+
import type { RunContext } from '../agent/context.js';
|
|
3
|
+
|
|
4
|
+
export const str = (args: Record<string, unknown>, key: string): string => {
|
|
5
|
+
const v = args[key];
|
|
6
|
+
if (typeof v !== 'string' || v === '') throw new Error(`missing required string arg "${key}"`);
|
|
7
|
+
return v;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// U+FFFD (the Unicode replacement character) is what a byte sequence becomes
|
|
11
|
+
// when UTF-8 decoding fails — most often a multibyte glyph (Ω, µ, ±, °) split
|
|
12
|
+
// across a streaming chunk boundary and decoded per-chunk upstream in the
|
|
13
|
+
// provider SDK (I2). It never appears in a legitimately authored PCB doc, so
|
|
14
|
+
// its presence in a content-bearing tool arg means the value arrived corrupted.
|
|
15
|
+
// Reject the call before it lands on disk so the model re-emits; the corruption
|
|
16
|
+
// is nondeterministic (it depends on where a chunk boundary fell), so the retry
|
|
17
|
+
// almost always comes through clean — far cheaper than shipping a mangled value
|
|
18
|
+
// like "5.1kΩ" → "5.1k�" into DECISIONS.md and only noticing on review.
|
|
19
|
+
const REPLACEMENT_CHAR = '�';
|
|
20
|
+
export function corruptionError(fields: Record<string, unknown>): string | null {
|
|
21
|
+
const bad = Object.entries(fields)
|
|
22
|
+
.filter(([, v]) => typeof v === 'string' && v.includes(REPLACEMENT_CHAR))
|
|
23
|
+
.map(([k]) => k);
|
|
24
|
+
if (!bad.length) return null;
|
|
25
|
+
return `rejected: the ${bad.join(', ')} value contains U+FFFD (�), the replacement character that signals a UTF-8 decoding error — a special character (e.g. Ω, µ, ±, °) was likely mangled in transit. Re-send this exact call with the intended character written correctly, or spell it in ASCII (e.g. "ohm", "uF", "+/-", "deg").`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function markTouched(ctx: RunContext, rel: string): void {
|
|
29
|
+
ctx.filesTouched.add(rel);
|
|
30
|
+
if (isKicadFile(rel)) {
|
|
31
|
+
ctx.ledger.onKicadEdit(rel);
|
|
32
|
+
if (rel.endsWith('.kicad_sch')) ctx.lastErc = null;
|
|
33
|
+
if (rel.endsWith('.kicad_pcb')) ctx.lastDrc = null;
|
|
34
|
+
} else if (rel.endsWith('.md')) {
|
|
35
|
+
ctx.ledger.onDocEdit(rel);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { outcomeResult, textResult, type ViewHint } from '../agent/envelope.js';
|
|
2
|
+
import { defineTool, type CatalogEntry, type CatalogTool } from './define.js';
|
|
3
|
+
import { HANDLERS } from './handlers.js';
|
|
4
|
+
import generateReport from './skills/generate-report.js';
|
|
5
|
+
|
|
6
|
+
export type { CatalogEntry, CatalogSkill, CatalogTool } from './define.js';
|
|
7
|
+
export { defineTool, defineSkill } from './define.js';
|
|
8
|
+
|
|
9
|
+
const HINT: Record<string, ViewHint> = {
|
|
10
|
+
read_file: 'query',
|
|
11
|
+
search: 'query',
|
|
12
|
+
list_symbols: 'query',
|
|
13
|
+
list_nets: 'query',
|
|
14
|
+
propose_change: 'mutation',
|
|
15
|
+
validate_change: 'diagnostic',
|
|
16
|
+
edit_file: 'mutation',
|
|
17
|
+
write_file: 'mutation',
|
|
18
|
+
run_erc: 'diagnostic',
|
|
19
|
+
search_symbols: 'query',
|
|
20
|
+
symbol_pins: 'query',
|
|
21
|
+
verify_symbols: 'diagnostic',
|
|
22
|
+
draft_schematic: 'mutation',
|
|
23
|
+
score_schematic: 'diagnostic',
|
|
24
|
+
check_legibility: 'diagnostic',
|
|
25
|
+
run_drc: 'diagnostic',
|
|
26
|
+
export_svg: 'export',
|
|
27
|
+
export_outputs: 'export',
|
|
28
|
+
check_drift: 'diagnostic',
|
|
29
|
+
record_constraint: 'mutation',
|
|
30
|
+
resolve_affected: 'mutation',
|
|
31
|
+
record_decision: 'mutation',
|
|
32
|
+
finish: 'diagnostic',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function wrap(def: (typeof HANDLERS)[number]): CatalogTool {
|
|
36
|
+
const viewHint = HINT[def.schema.name];
|
|
37
|
+
if (!viewHint) throw new Error(`missing viewHint for ${def.schema.name}`);
|
|
38
|
+
return defineTool({
|
|
39
|
+
schema: def.schema,
|
|
40
|
+
version: 1,
|
|
41
|
+
viewHint,
|
|
42
|
+
gate: def.requiresUnlock ? (ctx) => ctx.editsUnlocked : () => true,
|
|
43
|
+
handler: async (ctx, args) => {
|
|
44
|
+
const result = await def.handler(ctx, args);
|
|
45
|
+
return typeof result === 'string'
|
|
46
|
+
? textResult(result, viewHint)
|
|
47
|
+
: outcomeResult(result.text, result.ok, viewHint);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tools from HANDLERS + every skill module imported below (conformance checks skills/). */
|
|
53
|
+
export const catalog: CatalogEntry[] = [...HANDLERS.map(wrap), generateReport];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineSkill } from '../define.js';
|
|
2
|
+
|
|
3
|
+
export default defineSkill({
|
|
4
|
+
schema: {
|
|
5
|
+
name: 'generate_report',
|
|
6
|
+
description:
|
|
7
|
+
'Read-only report of the current design: ERC, DRC (if a board exists), drift, nets, and an optional SVG path. Does not edit files.',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: { scope: { type: 'string', enum: ['power', 'all'], description: 'Report scope (default all)' } },
|
|
11
|
+
required: [],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
version: 1,
|
|
15
|
+
viewHint: 'diagnostic',
|
|
16
|
+
tools: ['read_file', 'search', 'list_nets', 'run_erc', 'run_drc', 'export_svg', 'check_drift'],
|
|
17
|
+
maxTurns: 8,
|
|
18
|
+
prompt: (_ctx, args) => `You are generating a read-only design report (scope: ${args.scope === 'power' ? 'power' : 'all'}).
|
|
19
|
+
Call the available tools to gather ERC, DRC (only if a board is configured; otherwise skip), drift, and the net list. Optionally export an SVG of the schematic.
|
|
20
|
+
Do not edit any file. Do not call finish. When you have those results, stop calling tools.`,
|
|
21
|
+
isComplete: (ctx) =>
|
|
22
|
+
(!ctx.config.schematic || ctx.lastErc !== null) &&
|
|
23
|
+
(!ctx.config.board || ctx.lastDrc !== null) &&
|
|
24
|
+
(!ctx.config.schematic || ctx.lastDrift != null),
|
|
25
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -179,6 +179,83 @@ program
|
|
|
179
179
|
.description('ERC + DRC + doc-drift + spec validation; no LLM calls; CI-safe')
|
|
180
180
|
.action(checkAction);
|
|
181
181
|
|
|
182
|
+
// `draft` and `score` are command groups taking the artifact as a noun
|
|
183
|
+
// (`draft schematic` today, `draft pcb` when layout drafting exists), so the
|
|
184
|
+
// verb alone never has to guess what it applies to.
|
|
185
|
+
const draftGroup = program
|
|
186
|
+
.command('draft')
|
|
187
|
+
.description('deterministically draft an artifact from its declared intent; no LLM, no network');
|
|
188
|
+
draftGroup
|
|
189
|
+
.command('schematic')
|
|
190
|
+
.description('draft the schematic from schematic.intent.json')
|
|
191
|
+
.option('--intent <path>', 'repo-relative intent file (default: schematic.intent.json beside the schematic)')
|
|
192
|
+
.action(async (opts: { intent?: string }) => {
|
|
193
|
+
const repo = repoOf(program.opts());
|
|
194
|
+
const json = Boolean(program.opts().json);
|
|
195
|
+
try {
|
|
196
|
+
const { loadConfig } = await import('./config.js');
|
|
197
|
+
const { draftSchematic, defaultIntentPath, formatSchematicDraftReport } = await import('./kicad/draft/draft.js');
|
|
198
|
+
const config = await loadConfig(repo);
|
|
199
|
+
if (!config.schematic) {
|
|
200
|
+
console.error('no schematic configured in .copperhead/config.json');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const res = await draftSchematic({
|
|
204
|
+
repoRoot: repo,
|
|
205
|
+
schematic: config.schematic,
|
|
206
|
+
intentPath: opts.intent ?? defaultIntentPath(config.schematic),
|
|
207
|
+
docsDir: config.docs,
|
|
208
|
+
});
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
if (json) console.log(JSON.stringify({ ok: false, findings: res.findings }, null, 2));
|
|
211
|
+
else console.error(res.message);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
if (json) console.log(JSON.stringify({ ok: true, report: res.report }, null, 2));
|
|
215
|
+
else console.log(formatSchematicDraftReport(res.report));
|
|
216
|
+
process.exit(0);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error((err as Error).message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const scoreGroup = program
|
|
224
|
+
.command('score')
|
|
225
|
+
.description('quantitative quality score for an artifact; advisory exit code; no LLM, no network');
|
|
226
|
+
scoreGroup
|
|
227
|
+
.command('schematic')
|
|
228
|
+
.description('legibility and layout score for the schematic')
|
|
229
|
+
.option('--file <path>', 'score this .kicad_sch instead of the configured schematic (any sheet, no repo needed)')
|
|
230
|
+
.action(async (opts: { file?: string }) => {
|
|
231
|
+
const repo = repoOf(program.opts());
|
|
232
|
+
const json = Boolean(program.opts().json);
|
|
233
|
+
try {
|
|
234
|
+
const { loadConfig } = await import('./config.js');
|
|
235
|
+
const { scoreSchematic, formatScore } = await import('./kicad/score.js');
|
|
236
|
+
const path = await import('node:path');
|
|
237
|
+
if (opts.file) {
|
|
238
|
+
const report = await scoreSchematic(path.resolve(opts.file), { docsDir: null });
|
|
239
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatScore(report));
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
const config = await loadConfig(repo);
|
|
243
|
+
if (!config.schematic) {
|
|
244
|
+
console.error('no schematic configured in .copperhead/config.json');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
const report = await scoreSchematic(path.join(repo, config.schematic), {
|
|
248
|
+
docsDir: path.join(repo, config.docs),
|
|
249
|
+
...(config.legibility ? { config: config.legibility } : {}),
|
|
250
|
+
});
|
|
251
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatScore(report));
|
|
252
|
+
process.exit(0); // the exit code never depends on the composite (AC-16.26 family)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error((err as Error).message);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
182
259
|
program
|
|
183
260
|
.command('doctor')
|
|
184
261
|
.description('env preflight: kicad-cli, git, node, and the model provider credential; no LLM, no network')
|
|
@@ -240,6 +317,60 @@ program
|
|
|
240
317
|
},
|
|
241
318
|
);
|
|
242
319
|
|
|
320
|
+
const skillCmd = program.command('skill').description('run a registered skill (nested tool loop; no git commit)');
|
|
321
|
+
|
|
322
|
+
skillCmd
|
|
323
|
+
.command('list')
|
|
324
|
+
.description('list registered skills (LLM-free, network-free)')
|
|
325
|
+
.action(async () => {
|
|
326
|
+
const repo = repoOf(program.opts());
|
|
327
|
+
try {
|
|
328
|
+
const { listSkills } = await import('./commands/skill.js');
|
|
329
|
+
const skills = await listSkills(repo);
|
|
330
|
+
if (program.opts().json) console.log(JSON.stringify(skills, null, 2));
|
|
331
|
+
else {
|
|
332
|
+
for (const s of skills) {
|
|
333
|
+
console.log(`${s.available ? '·' : '×'} ${s.name.replaceAll('_', '-')} ${s.description.split('\n')[0]}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
process.exit(0);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error((err as Error).message);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
skillCmd
|
|
344
|
+
.command('run')
|
|
345
|
+
.description('run a skill by name (generate-report)')
|
|
346
|
+
.argument('<name>', 'skill name (kebab or underscore)')
|
|
347
|
+
.option('--model <model>', 'codex | cursor | gpt-5 | claude | claude-code | compat:<id>')
|
|
348
|
+
.option('--scope <scope>', 'generate-report scope: power | all', 'all')
|
|
349
|
+
.action(async (name: string, opts: { model?: string; scope?: string }) => {
|
|
350
|
+
const repo = repoOf(program.opts());
|
|
351
|
+
const json = Boolean(program.opts().json);
|
|
352
|
+
// `process.exit` skips pending finally blocks, so the provider close has to
|
|
353
|
+
// finish before the exit call — hence the code is carried out, not exited on.
|
|
354
|
+
let code = 1;
|
|
355
|
+
try {
|
|
356
|
+
const { runSkillCli, providerForSkillRun } = await import('./commands/skill.js');
|
|
357
|
+
const { provider } = await providerForSkillRun(repo, opts.model);
|
|
358
|
+
const res = await runSkillCli({
|
|
359
|
+
repoRoot: repo,
|
|
360
|
+
name,
|
|
361
|
+
args: { scope: opts.scope === 'power' ? 'power' : 'all' },
|
|
362
|
+
provider,
|
|
363
|
+
json,
|
|
364
|
+
});
|
|
365
|
+
console.log(res.text);
|
|
366
|
+
code = res.code;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.error((err as Error).message);
|
|
369
|
+
code = 1;
|
|
370
|
+
}
|
|
371
|
+
process.exit(code);
|
|
372
|
+
});
|
|
373
|
+
|
|
243
374
|
program
|
|
244
375
|
.command('sync')
|
|
245
376
|
.description('verify the whole design state for inconsistencies and resolve drift')
|
|
@@ -276,6 +407,29 @@ program
|
|
|
276
407
|
}
|
|
277
408
|
});
|
|
278
409
|
|
|
410
|
+
program
|
|
411
|
+
.command('mcp')
|
|
412
|
+
.description('EXPERIMENTAL: serve the gated pipeline to MCP hosts over stdio (unstable surface)')
|
|
413
|
+
// `--repo` is also a global flag, but a host config reads as
|
|
414
|
+
// `args: ["mcp", "--repo", "/path"]`, so the command accepts it locally too.
|
|
415
|
+
.option('--repo <path>', 'target repository (default: cwd)')
|
|
416
|
+
.action(async (opts: { repo?: string }) => {
|
|
417
|
+
const repo = repoOf(opts.repo ? { repo: opts.repo } : program.opts());
|
|
418
|
+
try {
|
|
419
|
+
// Imported lazily, like `skill`: the MCP SDK and zod are a ~150ms load
|
|
420
|
+
// that every other command — including the pre-commit `check` — would
|
|
421
|
+
// otherwise pay, and a resolution failure here would take down the whole
|
|
422
|
+
// CLI rather than just this command.
|
|
423
|
+
const { startMcpServer } = await import('./mcp/server.js');
|
|
424
|
+
// Never returns until the host closes stdio. Nothing is printed to
|
|
425
|
+
// stdout here or anywhere downstream: it carries JSON-RPC alone.
|
|
426
|
+
await startMcpServer({ repoRoot: repo });
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error((err as Error).message);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
279
433
|
program
|
|
280
434
|
.command('demo')
|
|
281
435
|
.description('tour of what copperhead does, or run the USB-C breakout create pipeline')
|
package/src/commands/check.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { runErc, runDrc } from '../kicad/cli.js';
|
|
|
5
5
|
import { formatViolations, type CheckReport } from '../kicad/report.js';
|
|
6
6
|
import { checkDrift, emptySchematicWarning, type DriftMismatch } from '../memory/drift.js';
|
|
7
7
|
import { loadConstraints, checkForbiddenPins, type ConstraintViolation } from '../memory/constraints.js';
|
|
8
|
-
import { pinNets } from '../kicad/sexp.js';
|
|
8
|
+
import { pinNets, readSheetGeometry } from '../kicad/sexp.js';
|
|
9
|
+
import { scoreFromGeometry, type ScoreReport } from '../kicad/score.js';
|
|
10
|
+
import { checkLegibility, formatLegibility, LEGIBILITY_FAMILIES, type LegibilityFinding } from '../kicad/legibility.js';
|
|
9
11
|
import { openspecValidate } from '../openspec/cli.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -19,6 +21,20 @@ export interface CheckResult {
|
|
|
19
21
|
drift: { ok: boolean; mismatches: DriftMismatch[]; warning?: string };
|
|
20
22
|
openspec: { ok: boolean; detail: string } | null;
|
|
21
23
|
constraints: { ok: boolean; violations: ConstraintViolation[] };
|
|
24
|
+
/**
|
|
25
|
+
* Advisory at every severity (design C6): findings inform, the exit code
|
|
26
|
+
* never depends on them, so existing repos gain information, not failures.
|
|
27
|
+
* Always present — all families skipped when no schematic is configured.
|
|
28
|
+
*/
|
|
29
|
+
legibility: {
|
|
30
|
+
findings: LegibilityFinding[];
|
|
31
|
+
counts: { error: number; advisory: number };
|
|
32
|
+
skipped: { family: string; reason: string }[];
|
|
33
|
+
disabled: string[];
|
|
34
|
+
suppressed: { family: string; sheet: string; count: number }[];
|
|
35
|
+
/** Advisory quantitative score; null when no schematic is configured. */
|
|
36
|
+
score: ScoreReport | null;
|
|
37
|
+
};
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
export async function runCheck(repoRoot: string, log: (s: string) => void): Promise<CheckResult> {
|
|
@@ -59,6 +75,39 @@ export async function runCheck(repoRoot: string, log: (s: string) => void): Prom
|
|
|
59
75
|
log(res.ok ? 'openspec ✓' : `openspec: ${res.output}`);
|
|
60
76
|
}
|
|
61
77
|
|
|
78
|
+
let legibility: CheckResult['legibility'];
|
|
79
|
+
if (config.schematic && existsSync(path.join(repoRoot, config.schematic))) {
|
|
80
|
+
const report = await checkLegibility(path.join(repoRoot, config.schematic), {
|
|
81
|
+
docsDir: path.join(repoRoot, config.docs),
|
|
82
|
+
...(config.legibility ? { config: config.legibility } : {}),
|
|
83
|
+
});
|
|
84
|
+
const score = scoreFromGeometry(
|
|
85
|
+
await readSheetGeometry(path.join(repoRoot, config.schematic)),
|
|
86
|
+
report,
|
|
87
|
+
config.legibility,
|
|
88
|
+
);
|
|
89
|
+
legibility = {
|
|
90
|
+
findings: report.findings,
|
|
91
|
+
counts: report.counts,
|
|
92
|
+
skipped: report.skipped,
|
|
93
|
+
disabled: report.disabled,
|
|
94
|
+
suppressed: report.suppressed,
|
|
95
|
+
score,
|
|
96
|
+
};
|
|
97
|
+
log(formatLegibility(report));
|
|
98
|
+
log(`legibility score: ${score.composite}/100${score.cap ? ` (capped: ${score.cap.reason})` : ''}`);
|
|
99
|
+
} else {
|
|
100
|
+
legibility = {
|
|
101
|
+
findings: [],
|
|
102
|
+
counts: { error: 0, advisory: 0 },
|
|
103
|
+
skipped: LEGIBILITY_FAMILIES.map((family) => ({ family, reason: 'no schematic configured' })),
|
|
104
|
+
disabled: [],
|
|
105
|
+
suppressed: [],
|
|
106
|
+
score: null,
|
|
107
|
+
};
|
|
108
|
+
log('legibility skipped (no schematic configured)');
|
|
109
|
+
}
|
|
110
|
+
|
|
62
111
|
let constraintViolations: ConstraintViolation[] = [];
|
|
63
112
|
if (config.schematic && existsSync(path.join(repoRoot, config.schematic))) {
|
|
64
113
|
const registry = await loadConstraints(repoRoot);
|
|
@@ -87,5 +136,6 @@ export async function runCheck(repoRoot: string, log: (s: string) => void): Prom
|
|
|
87
136
|
drift: { ok: drift.length === 0, mismatches: drift, ...(driftWarning ? { warning: driftWarning } : {}) },
|
|
88
137
|
openspec,
|
|
89
138
|
constraints: { ok: constraintViolations.length === 0, violations: constraintViolations },
|
|
139
|
+
legibility,
|
|
90
140
|
};
|
|
91
141
|
}
|