@grayhaven/nerve-cli 0.1.0 → 0.2.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 (2) hide show
  1. package/dist/index.js +281 -4
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -18,6 +18,30 @@ import {
18
18
  assemblyInstructions,
19
19
  boardSvg,
20
20
  bomCsv,
21
+ bopCsv,
22
+ bopJson,
23
+ analysisCsv,
24
+ analysisJson,
25
+ analyzeHarness,
26
+ builtinAdapters,
27
+ buildRecordJson,
28
+ contractJson,
29
+ createBuildRecord,
30
+ createRedline,
31
+ createRelease,
32
+ formboardSheets,
33
+ releaseJson,
34
+ ReleaseBlockedError,
35
+ resolveRedline,
36
+ suggestPatch,
37
+ validateRedlineTarget,
38
+ exportConnectorContract,
39
+ findAdapter,
40
+ generateQuote,
41
+ importPinoutCsv,
42
+ quoteCsv,
43
+ quoteJson,
44
+ validateContract,
21
45
  buildPacket,
22
46
  canRelease,
23
47
  cutListCsv,
@@ -92,9 +116,17 @@ Usage:
92
116
  nerve init [dir]
93
117
  nerve compile <file.harness.ts> [--out dir]
94
118
  nerve validate <file.harness.ts>
95
- nerve render <file.harness.ts> [--format svg] [--view schematic|board] [--out dir]
119
+ nerve render <file.harness.ts> [--format svg] [--view schematic|board|formboard] [--paper letter|a4] [--out dir]
96
120
  nerve export <file.harness.ts> [--target manufacturing-packet|wireviz] [--out dir]
97
121
  nerve import <file.yml> [--id harness-id] [--out dir] (WireViz YAML \u2192 HIR)
122
+ nerve quote <file.harness.ts> [--out dir] (requires costing in nerve.config.ts)
123
+ nerve analyze <file.harness.ts> [--out dir] (resistance, drop, bundle, weight \xA734)
124
+ nerve machine <adapter-id> <file.harness.ts> [--out dir] (shop-floor exports \xA731)
125
+ nerve contract <file.harness.ts> --connector <ref> [--against contract.json|pinout.csv] [--out dir]
126
+ nerve release <file.harness.ts> --eco <id> --reason <text> --date <iso> [--against release.json] [--out dir]
127
+ nerve record <file.harness.ts> --release <release.json> --serial <sn> --operator <name> --date <iso> --results <measurements.json> [--out dir]
128
+ nerve redline add <file.harness.ts> --target <hir-ref> --type <type> --description <text> [--value v] [--release id] [--serial sn]
129
+ nerve redline resolve <redlines.json> --id <id> --accept|--reject --reason <text> --date <iso>
98
130
  nerve diff <revA> <revB> [--json] (each: harness.json, .harness.ts, or revision dir)
99
131
  nerve inspect <harness.json>`;
100
132
  var loadHirForDiff = async (path, io) => {
@@ -150,13 +182,22 @@ var run = async (argv, io = realIo) => {
150
182
  return 2;
151
183
  }
152
184
  const view = flags["view"] ?? "schematic";
153
- if (view !== "schematic" && view !== "board") {
154
- io.err(`Unsupported render view: ${view} (supported: schematic, board)`);
185
+ if (view !== "schematic" && view !== "board" && view !== "formboard") {
186
+ io.err(`Unsupported render view: ${view} (supported: schematic, board, formboard)`);
155
187
  return 2;
156
188
  }
157
189
  const result = await compileOrExit(file, io);
158
190
  if (typeof result === "number") return result;
159
191
  const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
192
+ if (view === "formboard") {
193
+ const paper = flags["paper"] === "a4" ? "a4" : "letter";
194
+ const board = formboardSheets(result.hir, { paper });
195
+ for (const sheet of board.sheets) writeOut(outDir, sheet.name, sheet.svg, io);
196
+ io.out(
197
+ `formboard ${board.boardWidthMm}x${board.boardHeightMm} mm \u2192 ${board.rows}x${board.cols} ${paper} sheet(s) at 1:1. Print at 100% and verify the calibration ruler.`
198
+ );
199
+ return 0;
200
+ }
160
201
  if (view === "board") {
161
202
  writeOut(outDir, "board.svg", boardSvg(result.hir), io);
162
203
  } else {
@@ -192,7 +233,10 @@ var run = async (argv, io = realIo) => {
192
233
  }
193
234
  const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
194
235
  const tolerance = result.config.defaultWireTolerance;
195
- const options = tolerance !== void 0 ? { defaultWireTolerance: tolerance } : {};
236
+ const options = {
237
+ ...tolerance !== void 0 ? { defaultWireTolerance: tolerance } : {},
238
+ ...result.config.costing !== void 0 ? { costing: result.config.costing } : {}
239
+ };
196
240
  const plan = generateTestPlan(result.hir);
197
241
  writeOut(outDir, "harness.json", JSON.stringify(result.hir, null, 2) + "\n", io);
198
242
  writeOut(outDir, "schematic.svg", schematicSvg(result.hir), io);
@@ -200,9 +244,15 @@ var run = async (argv, io = realIo) => {
200
244
  writeOut(outDir, "bom.csv", bomCsv(result.hir), io);
201
245
  writeOut(outDir, "cut-list.csv", cutListCsv(result.hir, options), io);
202
246
  writeOut(outDir, "labels.csv", labelScheduleCsv(result.hir), io);
247
+ writeOut(outDir, "bop.csv", bopCsv(result.hir), io);
248
+ writeOut(outDir, "bop.json", bopJson(result.hir), io);
203
249
  writeOut(outDir, "tests.csv", testPlanCsv(plan), io);
204
250
  writeOut(outDir, "test-plan.json", testPlanJson(result.hir), io);
205
251
  writeOut(outDir, "assembly-instructions.txt", assemblyInstructions(result.hir), io);
252
+ if (result.config.costing !== void 0) {
253
+ writeOut(outDir, "quote.csv", quoteCsv(result.hir, result.config.costing), io);
254
+ writeOut(outDir, "quote.json", quoteJson(result.hir, result.config.costing), io);
255
+ }
206
256
  writeOut(outDir, "manufacturing-packet.pdf", await manufacturingPacketPdf(result.hir, options), io);
207
257
  writeOut(outDir, "manufacturing-packet.zip", (await buildPacket(result.hir, options)).zip, io);
208
258
  io.out(summarize(result.hir));
@@ -232,6 +282,233 @@ var run = async (argv, io = realIo) => {
232
282
  io.out(summarize(full));
233
283
  return hasErrors(diagnostics) ? 1 : 0;
234
284
  }
285
+ case "quote": {
286
+ const file = positional[0];
287
+ if (file === void 0) return usage(io);
288
+ const result = await compileOrExit(file, io);
289
+ if (typeof result === "number") return result;
290
+ const model = result.config.costing;
291
+ if (model === void 0) {
292
+ io.err("No costing model: add `costing: { laborRatePerHour, ... }` to nerve.config.ts (PRD \xA729).");
293
+ return 2;
294
+ }
295
+ const quote = generateQuote(result.hir, model);
296
+ const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
297
+ writeOut(outDir, "quote.csv", quoteCsv(result.hir, model), io);
298
+ writeOut(outDir, "quote.json", quoteJson(result.hir, model), io);
299
+ io.out(
300
+ `${quote.harness.id} rev ${quote.harness.revision} \u2014 material ${quote.materialCost.toFixed(2)} + scrap ${quote.scrapCost.toFixed(2)} + labor ${quote.laborCost.toFixed(2)} = ${quote.totalCost.toFixed(2)} ${quote.currency} (${quote.perUnitCost.toFixed(2)}/unit @ ${(quote.assumptions.yield * 100).toFixed(0)}% yield)`
301
+ );
302
+ for (const mpn of quote.longLeadItems) io.out(`LONG-LEAD: ${mpn}`);
303
+ for (const mpn of quote.lifecycleRisks) io.out(`LIFECYCLE: ${mpn}`);
304
+ for (const item of quote.unpricedItems) io.out(`UNPRICED: ${item}`);
305
+ return 0;
306
+ }
307
+ case "analyze": {
308
+ const file = positional[0];
309
+ if (file === void 0) return usage(io);
310
+ const result = await compileOrExit(file, io);
311
+ if (typeof result === "number") return result;
312
+ const report = analyzeHarness(result.hir);
313
+ const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
314
+ writeOut(outDir, "analysis.csv", analysisCsv(result.hir), io);
315
+ writeOut(outDir, "analysis.json", analysisJson(result.hir), io);
316
+ io.out(
317
+ `${report.harness.id} rev ${report.harness.revision} \u2014 ${report.totals.wireLengthM} m wire, ~${report.totals.estimatedWeightG} g, ${report.branches.map((b) => `${b.id}: \xD8${b.bundleDiameterMm}mm`).join(", ")}`
318
+ );
319
+ return 0;
320
+ }
321
+ case "machine": {
322
+ const [adapterId, file] = positional;
323
+ if (adapterId === void 0 || file === void 0) return usage(io);
324
+ const adapter = findAdapter(adapterId);
325
+ if (adapter === void 0) {
326
+ io.err(`Unknown adapter: ${adapterId}. Available: ${builtinAdapters.map((a) => a.id).join(", ")}`);
327
+ return 2;
328
+ }
329
+ const result = await compileOrExit(file, io);
330
+ if (typeof result === "number") return result;
331
+ const { files, diagnostics } = adapter.generate(result.hir);
332
+ printDiagnostics(diagnostics, io);
333
+ if (hasErrors(diagnostics)) return 1;
334
+ const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
335
+ for (const [name, contents] of files) writeOut(outDir, name, contents, io);
336
+ return 0;
337
+ }
338
+ case "contract": {
339
+ const file = positional[0];
340
+ const connectorRef = flags["connector"];
341
+ if (file === void 0 || connectorRef === void 0) return usage(io);
342
+ const result = await compileOrExit(file, io);
343
+ if (typeof result === "number") return result;
344
+ const against = flags["against"];
345
+ if (against !== void 0) {
346
+ let contract2;
347
+ try {
348
+ const raw = readFileSync(resolve(against), "utf8");
349
+ contract2 = against.endsWith(".csv") ? importPinoutCsv(raw, { connector: connectorRef }) : JSON.parse(raw);
350
+ } catch (cause) {
351
+ io.err(`Failed to load contract ${against}: ${cause instanceof Error ? cause.message : String(cause)}`);
352
+ return 2;
353
+ }
354
+ const diagnostics = validateContract(result.hir, contract2);
355
+ printDiagnostics(diagnostics, io);
356
+ io.out(
357
+ diagnostics.length === 0 ? `Connector ${connectorRef} conforms to ${against}.` : `${diagnostics.length} contract issue(s) for ${connectorRef}.`
358
+ );
359
+ return hasErrors(diagnostics) ? 1 : 0;
360
+ }
361
+ const contract = exportConnectorContract(result.hir, connectorRef);
362
+ if (contract === void 0) {
363
+ io.err(`Connector ${connectorRef} not found in ${result.hir.harness.id}.`);
364
+ return 2;
365
+ }
366
+ const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
367
+ writeOut(outDir, `contract-${connectorRef}.json`, contractJson(contract), io);
368
+ return 0;
369
+ }
370
+ case "release": {
371
+ const file = positional[0];
372
+ const eco = flags["eco"];
373
+ const reason = flags["reason"];
374
+ const date = flags["date"];
375
+ if (file === void 0 || eco === void 0 || reason === void 0 || date === void 0) return usage(io);
376
+ const result = await compileOrExit(file, io);
377
+ if (typeof result === "number") return result;
378
+ let previous;
379
+ if (flags["against"] !== void 0) {
380
+ try {
381
+ const prevRelease = JSON.parse(readFileSync(resolve(flags["against"]), "utf8"));
382
+ const prevDir = resolve(flags["against"], "..");
383
+ const prevHir = decodeHir(JSON.parse(readFileSync(join(prevDir, "harness.json"), "utf8")));
384
+ previous = { hir: prevHir, releaseId: prevRelease.releaseId };
385
+ } catch (cause) {
386
+ io.err(`Failed to load previous release: ${cause instanceof Error ? cause.message : String(cause)}`);
387
+ return 2;
388
+ }
389
+ }
390
+ try {
391
+ const release = createRelease(result.hir, {
392
+ eco: { id: eco, reason, ...flags["author"] !== void 0 ? { author: flags["author"] } : {} },
393
+ createdAt: date,
394
+ ...previous !== void 0 ? { previous } : {}
395
+ });
396
+ const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
397
+ writeOut(outDir, "harness.json", JSON.stringify(result.hir, null, 2) + "\n", io);
398
+ writeOut(outDir, `release-${result.hir.harness.revision}.json`, releaseJson(release), io);
399
+ io.out(
400
+ `Release ${release.releaseId} (${eco}) \u2014 fingerprint ${release.hirFingerprint}` + (release.impact !== void 0 ? ` \u2014 impact: ${release.impact.riskScore} (${release.impact.risk}), ${release.impact.pinoutChanges} pinout / ${release.impact.wireChanges} wire change(s)` : "")
401
+ );
402
+ return 0;
403
+ } catch (cause) {
404
+ if (cause instanceof ReleaseBlockedError) {
405
+ io.err(cause.message);
406
+ return 1;
407
+ }
408
+ throw cause;
409
+ }
410
+ }
411
+ case "record": {
412
+ const file = positional[0];
413
+ const releasePath = flags["release"];
414
+ const serial = flags["serial"];
415
+ const operator = flags["operator"];
416
+ const date = flags["date"];
417
+ const resultsPath = flags["results"];
418
+ if (file === void 0 || releasePath === void 0 || serial === void 0 || operator === void 0 || date === void 0 || resultsPath === void 0) {
419
+ return usage(io);
420
+ }
421
+ const result = await compileOrExit(file, io);
422
+ if (typeof result === "number") return result;
423
+ let release, measurements;
424
+ try {
425
+ release = JSON.parse(readFileSync(resolve(releasePath), "utf8"));
426
+ measurements = JSON.parse(readFileSync(resolve(resultsPath), "utf8"));
427
+ } catch (cause) {
428
+ io.err(`Failed to load inputs: ${cause instanceof Error ? cause.message : String(cause)}`);
429
+ return 2;
430
+ }
431
+ const record = createBuildRecord(result.hir, release, measurements, {
432
+ serial,
433
+ operator,
434
+ buildDate: date,
435
+ ...flags["lot"] !== void 0 ? { lot: flags["lot"] } : {},
436
+ ...flags["workstation"] !== void 0 ? { workstation: flags["workstation"] } : {}
437
+ });
438
+ const outDir = resolve(flags["out"] ?? result.config.outputDir ?? "dist");
439
+ writeOut(outDir, `build-record-${serial}.json`, buildRecordJson(record), io);
440
+ io.out(
441
+ `${serial}: ${record.summary.pass} pass / ${record.summary.fail} fail / ${record.summary.notRun} not run \u2192 ${record.summary.status.toUpperCase()}`
442
+ );
443
+ return record.summary.status === "fail" ? 1 : 0;
444
+ }
445
+ case "redline": {
446
+ const sub = positional[0];
447
+ if (sub === "add") {
448
+ const file = positional[1];
449
+ const target = flags["target"];
450
+ const type = flags["type"];
451
+ const description = flags["description"];
452
+ if (file === void 0 || target === void 0 || type === void 0 || description === void 0) return usage(io);
453
+ const result = await compileOrExit(file, io);
454
+ if (typeof result === "number") return result;
455
+ const invalid = validateRedlineTarget(result.hir, target);
456
+ if (invalid !== void 0) {
457
+ printDiagnostics([invalid], io);
458
+ return 1;
459
+ }
460
+ const redlinesPath = resolve(flags["file"] ?? "redlines.json");
461
+ const existing = existsSync(redlinesPath) ? JSON.parse(readFileSync(redlinesPath, "utf8")) : [];
462
+ const redline = createRedline({
463
+ id: `RL-${String(existing.length + 1).padStart(3, "0")}`,
464
+ target,
465
+ type,
466
+ description,
467
+ ...flags["value"] !== void 0 ? { proposedValue: flags["value"] } : {},
468
+ release: flags["release"] ?? `${result.hir.harness.id}@${result.hir.harness.revision}`,
469
+ ...flags["serial"] !== void 0 ? { serial: flags["serial"] } : {},
470
+ ...flags["by"] !== void 0 ? { reportedBy: flags["by"] } : {}
471
+ });
472
+ writeFileSync(redlinesPath, JSON.stringify([...existing, redline], null, 2) + "\n");
473
+ io.out(`Recorded ${redline.id} against ${target} in ${redlinesPath}`);
474
+ return 0;
475
+ }
476
+ if (sub === "resolve") {
477
+ const redlinesPath = positional[1];
478
+ const id = flags["id"];
479
+ const reason = flags["reason"];
480
+ const date = flags["date"];
481
+ const accept = flags["accept"] === "true";
482
+ const reject = flags["reject"] === "true";
483
+ if (redlinesPath === void 0 || id === void 0 || reason === void 0 || date === void 0 || accept === reject) return usage(io);
484
+ const redlines = JSON.parse(readFileSync(resolve(redlinesPath), "utf8"));
485
+ const existing = redlines.find((r) => r.id === id);
486
+ const index = redlines.findIndex((r) => r.id === id);
487
+ if (existing === void 0) {
488
+ io.err(`Redline ${id} not found in ${redlinesPath}.`);
489
+ return 2;
490
+ }
491
+ const resolved = resolveRedline(existing, {
492
+ accept,
493
+ reason,
494
+ resolvedAt: date,
495
+ ...flags["by"] !== void 0 ? { by: flags["by"] } : {}
496
+ });
497
+ const updated = [...redlines];
498
+ updated[index] = resolved;
499
+ writeFileSync(resolve(redlinesPath), JSON.stringify(updated, null, 2) + "\n");
500
+ io.out(`${id} ${resolved.status}.`);
501
+ if (resolved.status === "accepted") {
502
+ const patch = suggestPatch(resolved);
503
+ if (patch !== void 0) {
504
+ io.out("Structured patch (apply via variant() or edit the source):");
505
+ io.out(JSON.stringify(patch, null, 2));
506
+ }
507
+ }
508
+ return 0;
509
+ }
510
+ return usage(io);
511
+ }
235
512
  case "diff": {
236
513
  const [pathA, pathB] = positional;
237
514
  if (pathA === void 0 || pathB === void 0) return usage(io);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grayhaven/nerve-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Grayhaven Nerve CLI: compile, validate, render, and export harnesses locally and in CI.",
6
6
  "bin": {
@@ -15,16 +15,16 @@
15
15
  "dependencies": {
16
16
  "effect": "^3.16.0",
17
17
  "jiti": "^2.4.2",
18
- "@grayhaven/nerve-exporters": "0.1.0",
19
- "@grayhaven/nerve": "0.1.0",
20
- "@grayhaven/nerve-wireviz": "0.1.0",
21
- "@grayhaven/nerve-compiler": "0.1.0"
18
+ "@grayhaven/nerve": "0.2.0",
19
+ "@grayhaven/nerve-wireviz": "0.2.0",
20
+ "@grayhaven/nerve-exporters": "0.2.0",
21
+ "@grayhaven/nerve-compiler": "0.2.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.9.2",
25
25
  "typescript": "^5.8.3",
26
26
  "vitest": "^3.2.4",
27
- "@grayhaven/nerve-connectors": "0.1.0"
27
+ "@grayhaven/nerve-connectors": "0.2.0"
28
28
  },
29
29
  "license": "Apache-2.0",
30
30
  "files": [