deadbyte-mcp 0.15.21 → 0.15.23
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/MANIFEST.SHA256 +53 -28
- package/PROVENANCE.json +10 -10
- package/README.txt +18 -7
- package/RELEASE.json +51 -16
- package/SBOM.cdx.json +33 -7
- package/TEST-MATRIX.json +95 -18
- package/bin/deadbyte-hardware-observer-r45.exe +0 -0
- package/bin/deadbyte-hardware-observer-r45.obj +0 -0
- package/docs/R48-FULL-TOOL-CATALOG.json +124 -0
- package/native/deadbyte-hardware-observer.cpp +535 -12
- package/package.json +95 -93
- package/scripts/activate-skill-creator-1.1.1.mjs +46 -0
- package/scripts/build-advanced-pcb-skill.mjs +208 -0
- package/scripts/build-hardware-r45.ps1 +19 -0
- package/scripts/diagnose-skill-tree.mjs +8 -0
- package/scripts/full-regression-r48-batch.mjs +27 -0
- package/scripts/npm-release-r48.mjs +252 -0
- package/scripts/pcb-skill-src-SKILL.md.txt +67 -0
- package/scripts/pcb-skill-src-bringup-evidence.md.txt +22 -0
- package/scripts/pcb-skill-src-constraint-model.md.txt +17 -0
- package/scripts/pcb-skill-src-emc-mechanical-dfm.md.txt +17 -0
- package/scripts/pcb-skill-src-high-speed-layout.md.txt +27 -0
- package/scripts/pcb-skill-src-kicad-native.md.txt +30 -0
- package/scripts/pcb-skill-src-kicad_pcb_probe.py.txt +159 -0
- package/scripts/pcb-skill-src-pcb_constraint_gate.py.txt +76 -0
- package/scripts/pcb-skill-src-power-integrity.md.txt +15 -0
- package/scripts/prepare-skill-creator-1.1.1.mjs +113 -0
- package/scripts/promote-local-c-generation.ps1 +217 -0
- package/scripts/promote-local-d-generation.ps1 +217 -0
- package/scripts/test-r48-post-bump.ps1 +8 -0
- package/scripts/test-r48-windows-observation.ps1 +8 -0
- package/scripts/test-skill-creator-semver-matrix.mjs +30 -0
- package/scripts/test-skill-creator-semver.mjs +22 -0
- package/src/capability-ledger.mjs +7 -0
- package/src/capability-resolver.mjs +7 -0
- package/src/compact-gateway-mcp-tools.mjs +16 -2
- package/src/deadbyte-cli.mjs +70 -15
- package/src/machine-fs.mjs +16 -3
- package/src/observation-mcp-tools.mjs +13 -1
- package/src/observation-output-contracts.mjs +8 -0
- package/src/observation-runtime.mjs +54 -2
- package/src/remote-live-log.mjs +189 -35
- package/src/tool-audit.mjs +1 -1
- package/src/version.mjs +1 -1
- package/test/full-max-catalog-r47.test.mjs +1 -1
- package/test/full-max-catalog-r48.test.mjs +78 -0
- package/test/machine-fs-runtime.test.mjs +28 -3
- package/test/observation-mcp-tools.test.mjs +4 -3
- package/test/r48-release-contract.test.mjs +24 -14
- package/test/remote-live-log-r37.test.mjs +106 -7
- package/test/remote-session-cli.test.mjs +40 -0
- package/test/tool-audit.test.mjs +17 -0
- package/test/windows-observation-r48.test.mjs +63 -0
- package/test/windows-observation-surface-r48.test.mjs +70 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, readFile, rm, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
8
|
+
const node = process.execPath;
|
|
9
|
+
const npmCli = process.env.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
10
|
+
|
|
11
|
+
function run(command, args, options = {}) {
|
|
12
|
+
const { cwd = root, timeout = 180000 } = options;
|
|
13
|
+
const r = spawnSync(command, args, {
|
|
14
|
+
cwd,
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
timeout,
|
|
17
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
18
|
+
shell: false,
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
if (r.error) throw r.error;
|
|
22
|
+
if (r.status !== 0) {
|
|
23
|
+
const detail = (r.stderr || r.stdout || '').slice(-12000);
|
|
24
|
+
throw new Error(`${command} ${args.join(' ')} failed (exit=${r.status}, cwd=${cwd}):\n${detail}`);
|
|
25
|
+
}
|
|
26
|
+
return (r.stdout || '').trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runNpm(args, options = {}) {
|
|
30
|
+
return run(node, [npmCli, ...args], options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runNpmInteractive(args, options = {}) {
|
|
34
|
+
const { cwd = root, timeout = 300000 } = options;
|
|
35
|
+
const r = spawnSync(node, [npmCli, ...args], {
|
|
36
|
+
cwd,
|
|
37
|
+
timeout,
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
shell: false,
|
|
40
|
+
windowsHide: false,
|
|
41
|
+
});
|
|
42
|
+
if (r.error) throw r.error;
|
|
43
|
+
if (r.status !== 0) throw new Error('Interactive npm command failed (exit=' + r.status + ', cwd=' + cwd + ')');
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseJson(text) {
|
|
48
|
+
const start = Math.min(...[text.indexOf('{'), text.indexOf('[')].filter(n => n >= 0));
|
|
49
|
+
if (!Number.isFinite(start)) throw new Error('Command JSON output missing:\n' + text.slice(0, 1000));
|
|
50
|
+
return JSON.parse(text.slice(start));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const allowedUnmanifested = new Set([
|
|
54
|
+
'MANIFEST.SHA256',
|
|
55
|
+
'controller/deadbyte-host-policy.json',
|
|
56
|
+
'controller/deadbyte-coding-policy.json',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const isVerifyOnly = process.argv.includes('--verify-only') || process.argv.includes('--dry-run');
|
|
61
|
+
|
|
62
|
+
console.log(`[1/7] Reading package.json and checking registry publication status... ${isVerifyOnly ? '(MODE: VERIFY-ONLY / DRY-RUN)' : '(MODE: PUBLISH)'}`);
|
|
63
|
+
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
64
|
+
const version = pkg.version;
|
|
65
|
+
if (pkg.name !== 'deadbyte-mcp') throw new Error(`unexpected package name: ${pkg.name}`);
|
|
66
|
+
|
|
67
|
+
const releaseBytes = await readFile(path.join(root, 'RELEASE.json'));
|
|
68
|
+
if (releaseBytes.subarray(0, 3).equals(Buffer.from([0xEF, 0xBB, 0xBF]))) {
|
|
69
|
+
throw new Error('RELEASE.json must be UTF-8 without BOM');
|
|
70
|
+
}
|
|
71
|
+
const release = JSON.parse(releaseBytes.toString('utf8'));
|
|
72
|
+
const provenance = JSON.parse(await readFile(path.join(root, 'PROVENANCE.json'), 'utf8'));
|
|
73
|
+
if (release.schema !== 'deadbyte.release.v1' || release.name !== pkg.name) {
|
|
74
|
+
throw new Error('RELEASE.json schema/package identity mismatch');
|
|
75
|
+
}
|
|
76
|
+
if (release.version !== version) {
|
|
77
|
+
throw new Error(`RELEASE.json version mismatch: package=${version} release=${release.version}`);
|
|
78
|
+
}
|
|
79
|
+
if (provenance.version !== version) {
|
|
80
|
+
throw new Error(`PROVENANCE.json version mismatch: package=${version} provenance=${provenance.version}`);
|
|
81
|
+
}
|
|
82
|
+
if (release.parent?.release_id !== provenance.parent_release_id ||
|
|
83
|
+
release.parent?.manifest_sha256 !== provenance.parent_manifest_sha256 ||
|
|
84
|
+
release.parent?.generation_manifest_sha256 !== provenance.parent_generation_manifest_sha256) {
|
|
85
|
+
throw new Error('RELEASE.json parent identity does not match sealed PROVENANCE.json lineage');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const existing = spawnSync(node, [npmCli, 'view', `deadbyte-mcp@${version}`, 'version', '--json'], {
|
|
89
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: false, windowsHide: true,
|
|
90
|
+
});
|
|
91
|
+
const existingText = (existing.stdout || '').trim();
|
|
92
|
+
const isAlreadyPublished = existing.status === 0 && existingText && existingText !== 'null';
|
|
93
|
+
|
|
94
|
+
if (isAlreadyPublished) {
|
|
95
|
+
if (!isVerifyOnly) {
|
|
96
|
+
throw new Error(`deadbyte-mcp@${version} is already published on registry: ${existingText}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(`Notice: deadbyte-mcp@${version} is already published on registry (${existingText}), proceeding with pre-publish verification only.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('[2/7] Pre-pack gates: verifying working tree manifest and doctor...');
|
|
103
|
+
run(node, [path.join(root, 'scripts', 'release-manifest.mjs'), 'verify']);
|
|
104
|
+
run(node, [path.join(root, 'src', 'deadbyte-cli.mjs'), 'doctor']);
|
|
105
|
+
|
|
106
|
+
console.log('[3/7] Packing artifact with npm pack...');
|
|
107
|
+
const packed = parseJson(runNpm(['pack', '--json'], { timeout: 180000 }));
|
|
108
|
+
const filename = packed?.[0]?.filename;
|
|
109
|
+
if (!filename) throw new Error('npm pack did not return a tarball filename');
|
|
110
|
+
const tarball = path.join(root, filename);
|
|
111
|
+
let releaseSucceeded = false;
|
|
112
|
+
let publishAttempted = false;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
console.log('[4/7] Verifying packed artifact file surface against MANIFEST.SHA256 and packaging contract...');
|
|
116
|
+
const manifestText = await readFile(path.join(root, 'MANIFEST.SHA256'), 'utf8');
|
|
117
|
+
const manifestEntries = new Set();
|
|
118
|
+
for (const line of manifestText.split(/\r?\n/)) {
|
|
119
|
+
if (!line) continue;
|
|
120
|
+
const m = /^([0-9a-f]{64})\s+(.+)$/.exec(line);
|
|
121
|
+
if (m) manifestEntries.add(m[2]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tarListing = run('tar', ['-tf', tarball], { timeout: 60000 });
|
|
125
|
+
const tarFiles = new Set(
|
|
126
|
+
tarListing
|
|
127
|
+
.split(/\r?\n/)
|
|
128
|
+
.map(s => s.trim().replaceAll('\\', '/'))
|
|
129
|
+
.filter(s => s.startsWith('package/') && s.length > 'package/'.length && !s.endsWith('/'))
|
|
130
|
+
.map(s => s.slice('package/'.length))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const missingInTarball = [];
|
|
134
|
+
for (const rel of manifestEntries) {
|
|
135
|
+
if (!tarFiles.has(rel)) missingInTarball.push(rel);
|
|
136
|
+
}
|
|
137
|
+
if (missingInTarball.length > 0) {
|
|
138
|
+
throw new Error(`Tarball missing files declared in manifest (${missingInTarball.length}): ${missingInTarball.slice(0, 10).join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const unexpectedInTarball = [];
|
|
142
|
+
for (const rel of tarFiles) {
|
|
143
|
+
if (!manifestEntries.has(rel) && !allowedUnmanifested.has(rel)) {
|
|
144
|
+
unexpectedInTarball.push(rel);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (unexpectedInTarball.length > 0) {
|
|
148
|
+
throw new Error(`Tarball contains unexpected files not in manifest or allowlist (${unexpectedInTarball.length}): ${unexpectedInTarball.slice(0, 10).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`Packed artifact surface verified OK: ${tarFiles.size} package files, all ${manifestEntries.size} manifest entries present.`);
|
|
152
|
+
|
|
153
|
+
console.log('[5/7] Pre-publish artifact isolation gate in sandbox...');
|
|
154
|
+
const sandbox = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-release-verify-'));
|
|
155
|
+
try {
|
|
156
|
+
run('tar', ['-xzf', tarball, '-C', sandbox]);
|
|
157
|
+
const extractedPackage = path.join(sandbox, 'package');
|
|
158
|
+
|
|
159
|
+
const extractedVersion = run(node, [path.join(extractedPackage, 'src', 'deadbyte-cli.mjs'), '--version'], {
|
|
160
|
+
cwd: extractedPackage,
|
|
161
|
+
});
|
|
162
|
+
if (extractedVersion !== version) {
|
|
163
|
+
throw new Error(`Isolated package version mismatch: expected=${version} got=${extractedVersion}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
run(node, [path.join(extractedPackage, 'src', 'deadbyte-cli.mjs'), 'doctor'], {
|
|
167
|
+
cwd: extractedPackage,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
console.log('Artifact isolation gate PASS (clean doctor and version check on unpacked tarball in sandbox)');
|
|
171
|
+
} finally {
|
|
172
|
+
await rm(sandbox, { recursive: true, force: true }).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isVerifyOnly) {
|
|
176
|
+
console.log('[6/7] [VERIFY-ONLY] Skipping npm publish.');
|
|
177
|
+
console.log('[7/7] [VERIFY-ONLY] Skipping external registry smoke test.');
|
|
178
|
+
console.log('\nVERIFY-ONLY PIPELINE PASS: All pre-pack, artifact surface, and isolated sandbox gates passed successfully.');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('[6/7] Publishing artifact to npm registry...');
|
|
183
|
+
publishAttempted = true;
|
|
184
|
+
let publishOut;
|
|
185
|
+
try {
|
|
186
|
+
publishOut = runNpmInteractive(['publish', tarball, '--access', 'public', '--auth-type=web'], { timeout: 300000 });
|
|
187
|
+
} catch (publishError) {
|
|
188
|
+
const detail = String(publishError?.message || publishError);
|
|
189
|
+
if (/EOTP|one-time password|two-factor|2FA|security.?key|authentication/i.test(detail)) {
|
|
190
|
+
console.error('Publish requires interactive npm authentication (Security Key/WebAuthn).');
|
|
191
|
+
console.error('Tarball preserved for retry: ' + tarball);
|
|
192
|
+
}
|
|
193
|
+
throw publishError;
|
|
194
|
+
}
|
|
195
|
+
const published = parseJson(runNpm(['view', `deadbyte-mcp@${version}`, 'version', 'dist.integrity', 'dist.tarball', '--json'], { timeout: 120000 }));
|
|
196
|
+
const tags = parseJson(runNpm(['view', 'deadbyte-mcp', 'dist-tags', '--json'], { timeout: 120000 }));
|
|
197
|
+
|
|
198
|
+
console.log('[7/7] Post-publish external registry smoke test (executed outside repo in temp)...');
|
|
199
|
+
const tempDir = os.tmpdir();
|
|
200
|
+
const externalVersion = runNpm(['exec', '--yes', `--package=deadbyte-mcp@${version}`, '--', 'deadbyte-mcp', '--version'], {
|
|
201
|
+
cwd: tempDir,
|
|
202
|
+
timeout: 300000,
|
|
203
|
+
});
|
|
204
|
+
if (externalVersion !== version) {
|
|
205
|
+
throw new Error(`External registry smoke test version mismatch: expected=${version} got=${externalVersion}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const externalDoctor = runNpm(['exec', '--yes', `--package=deadbyte-mcp@${version}`, '--', 'deadbyte-mcp', 'doctor'], {
|
|
209
|
+
cwd: tempDir,
|
|
210
|
+
timeout: 300000,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
console.log('[8/8] Activating the published release into the live immutable slot and waiting for verified handoff...');
|
|
214
|
+
run(node, [path.join(root, 'scripts', 'deploy-r48.mjs')], { cwd: root, timeout: 900000 });
|
|
215
|
+
|
|
216
|
+
releaseSucceeded = true;
|
|
217
|
+
console.log('\nRELEASE SUCCESSFUL:');
|
|
218
|
+
console.log(JSON.stringify({
|
|
219
|
+
status: 'published',
|
|
220
|
+
name: pkg.name,
|
|
221
|
+
version,
|
|
222
|
+
tarball: filename,
|
|
223
|
+
published,
|
|
224
|
+
published_output_tail: publishOut.slice(-2000),
|
|
225
|
+
dist_tags: tags,
|
|
226
|
+
external_smoke: {
|
|
227
|
+
version_output: externalVersion,
|
|
228
|
+
doctor_status: 'ok',
|
|
229
|
+
},
|
|
230
|
+
}, null, 2));
|
|
231
|
+
|
|
232
|
+
} finally {
|
|
233
|
+
if (releaseSucceeded || !publishAttempted) {
|
|
234
|
+
await unlink(tarball).catch(() => {});
|
|
235
|
+
} else {
|
|
236
|
+
console.error('Release artifact preserved after failure: ' + tarball);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main().catch(err => {
|
|
242
|
+
console.error('\nRELEASE ABORTED / FAILED:');
|
|
243
|
+
console.error(err?.stack || err);
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deadbyte-advanced-pcb-engineering
|
|
3
|
+
description: Evidence-driven advanced PCB engineering workflow for schematic review, constraint capture, stackup-aware placement and routing, power and signal integrity, EMC, DFM/DFT, KiCad verification, manufacturing release, and hardware bring-up.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
audience: agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# DEADBYTE ADVANCED PCB ENGINEERING
|
|
9
|
+
|
|
10
|
+
Use this skill for serious PCB design, review, rework, manufacturing preparation, and board bring-up. It is optimized for KiCad source artifacts but the engineering workflow applies to other EDA tools when equivalent primary evidence is available.
|
|
11
|
+
|
|
12
|
+
## Core law
|
|
13
|
+
|
|
14
|
+
The schematic expresses intent. The board file expresses implementation. ERC, DRC, manufacturing outputs, measurements, and hardware observations are evidence.
|
|
15
|
+
|
|
16
|
+
Never invent a stackup, dielectric constant, loss tangent, copper thickness, trace impedance, current capacity, thermal rise, manufacturer capability, timing budget, or connector pinout. Mark missing inputs as UNKNOWN and stop the affected calculation or claim.
|
|
17
|
+
|
|
18
|
+
Do not call a board ready because it renders, routes, exports Gerbers, or passes one checker.
|
|
19
|
+
|
|
20
|
+
## Operating loop
|
|
21
|
+
|
|
22
|
+
1. Freeze board identity: project, revision, source hashes, EDA version, target manufacturer, board class, environment, and physical envelope.
|
|
23
|
+
2. Extract the electrical interface matrix from the schematic: rails, clocks, resets, boot straps, buses, differential pairs, analog nodes, RF, connectors, test points, and programming paths.
|
|
24
|
+
3. Write the constraint contract before routing. Separate electrical, stackup, fabrication, assembly, mechanical, thermal, and test constraints.
|
|
25
|
+
4. Establish stackup and reference-plane assumptions before impedance or high-speed routing.
|
|
26
|
+
5. Place from physics outward: connectors and mechanical locks, power entry, clocks, high-speed endpoints, regulators, memory, analog, then low-speed logic.
|
|
27
|
+
6. Establish return-current paths before route aesthetics. Do not cross plane splits with fast signals unless the return path is explicitly engineered.
|
|
28
|
+
7. Close power integrity: source path, protection, regulation, bulk storage, local decoupling, high-frequency loop area, transient current, copper loss, and thermal dissipation.
|
|
29
|
+
8. Route critical interfaces from their actual edge rate and topology. Length matching is subordinate to topology, reference continuity, coupling, and timing budget.
|
|
30
|
+
9. Close EMC and ESD paths at the boundary. Keep noisy current loops local. Give external transients a short controlled path away from sensitive logic.
|
|
31
|
+
10. Add DFM and DFT evidence: clearances, annular rings, drill limits, solder-mask policy, fiducials, tooling, test points, programming access, polarity/orientation checks, and rework access.
|
|
32
|
+
11. Run deterministic board probe, EDA ERC/DRC, and the declared constraint gate. Preserve exact outputs and hashes.
|
|
33
|
+
12. Export fabrication and assembly artifacts only after source verification. Hash the release set and record the exact source revision that produced it.
|
|
34
|
+
13. Build a bring-up plan before power is applied. Start with shorts and rail resistance, current-limited power, sequencing, clocks, resets, boot straps, debug access, then peripherals.
|
|
35
|
+
14. Close each claim with observed evidence. Report PASS, FAIL, PARTIALLY VERIFIED, BLOCKED BY EVIDENCE, or HYPOTHESIS ONLY.
|
|
36
|
+
|
|
37
|
+
## Evidence hierarchy
|
|
38
|
+
|
|
39
|
+
Prefer datasheets and package drawings for external facts; native schematic and PCB sources for implemented connectivity and geometry; EDA ERC/DRC for rule checking; deterministic source inspection for structural observations; manufacturing outputs for release consistency; bench measurements for physical behavior.
|
|
40
|
+
|
|
41
|
+
A screenshot is weak evidence. A rendered trace is not a measured impedance. A DRC pass is not signal-integrity proof.
|
|
42
|
+
|
|
43
|
+
## High-speed rule
|
|
44
|
+
|
|
45
|
+
Before tuning a fast net, identify driver, receiver, signaling standard, edge rate, topology, reference plane, layer transition, via structure, stackup geometry, termination, timing budget, and allowed discontinuities. If any parameter required for a numerical claim is missing, do not fabricate the number.
|
|
46
|
+
|
|
47
|
+
## Bundled references
|
|
48
|
+
|
|
49
|
+
Load only what the task needs:
|
|
50
|
+
- references/constraint-model.md
|
|
51
|
+
- references/high-speed-layout.md
|
|
52
|
+
- references/power-integrity.md
|
|
53
|
+
- references/emc-mechanical-dfm.md
|
|
54
|
+
- references/bringup-evidence.md
|
|
55
|
+
- references/kicad-native.md
|
|
56
|
+
|
|
57
|
+
## Deterministic helpers
|
|
58
|
+
|
|
59
|
+
Use scripts/kicad_pcb_probe.py on a native KiCad PCB file. It performs a non-executing S-expression parse and emits source hash, format identity, layer list, net names, footprint/segment/via/zone counts, track-width range, and via geometry range.
|
|
60
|
+
|
|
61
|
+
Use scripts/pcb_constraint_gate.py with a JSON contract to compare observed source metrics against explicit minima and required names. The gate can prove only the rules it implements. It does not replace ERC, DRC, field solvers, thermal analysis, or bench measurement.
|
|
62
|
+
|
|
63
|
+
## Completion contract
|
|
64
|
+
|
|
65
|
+
Before claiming FAB READY or BOARD VERIFIED, record exact source hashes, exact EDA/tool versions, ERC and DRC results, constraint-gate result, unresolved waivers, fabrication and assembly output hashes, unverified SI/PI/thermal/EMC/mechanical assumptions, and physical bring-up evidence for hardware-dependent claims.
|
|
66
|
+
|
|
67
|
+
Never weaken a rule or verifier to make the board pass.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Release and Hardware Bring-Up Evidence
|
|
2
|
+
|
|
3
|
+
A pre-fabrication release should bind source PCB/schematic hashes, project revision, BOM, fabrication outputs, drill outputs, pick-and-place, assembly drawings, fabrication notes/stackup, mechanical exchange artifacts where used, ERC/DRC logs, explicit waivers, and tool versions.
|
|
4
|
+
|
|
5
|
+
Regenerate the bundle from the exact released source revision. Do not mix old exports.
|
|
6
|
+
|
|
7
|
+
Bring-up order:
|
|
8
|
+
1. Inspect polarity, orientation, bridges, missing parts, and damaged connectors.
|
|
9
|
+
2. Measure resistance from major rails to ground and investigate suspicious lows.
|
|
10
|
+
3. Use a current-limited supply for first power where the design permits.
|
|
11
|
+
4. Verify input protection and primary rails.
|
|
12
|
+
5. Verify sequencing and enables.
|
|
13
|
+
6. Verify reset.
|
|
14
|
+
7. Verify oscillators/clocks with a probing method that does not invalidate the measurement.
|
|
15
|
+
8. Verify boot straps and debug/programming access.
|
|
16
|
+
9. Program the smallest known-good image.
|
|
17
|
+
10. Bring up one peripheral class at a time.
|
|
18
|
+
11. Record current consumption, temperatures, rails, and anomalies.
|
|
19
|
+
|
|
20
|
+
Physical PASS requires observed hardware evidence. DRC and source review reduce risk but do not substitute for power-on behavior.
|
|
21
|
+
|
|
22
|
+
When a board fails, freeze the failing revision and evidence before rework. Record bodges and substitutions.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# PCB Constraint Model
|
|
2
|
+
|
|
3
|
+
Treat constraints as source data, not prose hidden in chat history.
|
|
4
|
+
|
|
5
|
+
Record board identity: revision, EDA format/version, intended fab, assembly process, environmental range, input power, mechanical envelope, layer count target, controlled-impedance requirement, and safety/isolation class if any.
|
|
6
|
+
|
|
7
|
+
For every critical interface record endpoints and pins, signaling standard, voltage, direction, topology, data rate, edge rate, impedance target only when stackup data exists, reference plane, allowed layers, via structure, termination, timing/skew budget, spacing/isolation, boundary protection, and probe requirement.
|
|
8
|
+
|
|
9
|
+
For every rail record nominal voltage and tolerance, source/regulator, steady and transient current estimates, sequencing, bulk and local decoupling, sensitive loads, copper path, connector limits, dissipation, and measurement points.
|
|
10
|
+
|
|
11
|
+
Separate datasheet rules, fabrication rules, design rules, and evidence rules. Conflicts are not averaged. Resolve which rule governs.
|
|
12
|
+
|
|
13
|
+
Do not infer controlled impedance from trace width alone. Width without dielectric geometry and material parameters is insufficient.
|
|
14
|
+
|
|
15
|
+
Do not use generic current charts as a blind oracle. Temperature rise depends on geometry, copper thickness, plane coupling, airflow, ambient, nearby heat, and reliability margin.
|
|
16
|
+
|
|
17
|
+
A numeric rule without provenance is a hypothesis, not a constraint.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# EMC, Mechanical, DFM, and DFT
|
|
2
|
+
|
|
3
|
+
Treat every external cable, connector, shield, and exposed conductor as an EMC/ESD boundary.
|
|
4
|
+
|
|
5
|
+
Place transient protection where energy enters. Give surge and ESD current a short low-inductance path to its intended return. Do not route discharge through sensitive logic by accident.
|
|
6
|
+
|
|
7
|
+
Keep common-mode conversion low by preserving pair symmetry and reference continuity.
|
|
8
|
+
|
|
9
|
+
Name the current path instead of arguing from labels such as analog ground or digital ground. A split plane is justified only when the complete signal and return topology supports it.
|
|
10
|
+
|
|
11
|
+
Mechanical review must verify connector mating direction and insertion envelope, mounting-hole keepouts, screw-head clearance, component height, board-edge clearance, heatsink/airflow space, cable bend/strain relief, and test/programming access after assembly.
|
|
12
|
+
|
|
13
|
+
Bind fabrication rules to the chosen fab: trace/space, copper, drill, annular ring, aspect ratio, solder mask, copper-to-edge, impedance service, controlled-depth operations, finish, material, and panel constraints. Do not silently rely on generic KiCad defaults.
|
|
14
|
+
|
|
15
|
+
Assembly review must cover package orientation, pin-1 evidence, polarized parts, courtyard conflicts, solderability, paste exceptions, fiducials, side restrictions, and rework access.
|
|
16
|
+
|
|
17
|
+
Expose test points for critical rails, reset, clock, boot straps, programming/debug, and important buses where practical.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# High-Speed Layout
|
|
2
|
+
|
|
3
|
+
High speed is set by transition time and interconnect electrical length, not just protocol bit rate.
|
|
4
|
+
|
|
5
|
+
Route decision order:
|
|
6
|
+
1. Topology.
|
|
7
|
+
2. Reference continuity.
|
|
8
|
+
3. Placement and pin escape.
|
|
9
|
+
4. Discontinuities and layer changes.
|
|
10
|
+
5. Coupling and spacing.
|
|
11
|
+
6. Termination.
|
|
12
|
+
7. Timing and skew.
|
|
13
|
+
8. Cosmetic length matching.
|
|
14
|
+
|
|
15
|
+
A perfectly length-matched pair over a broken return path is still wrong.
|
|
16
|
+
|
|
17
|
+
Identify the reference plane for every fast route segment. When changing reference planes, provide a nearby return transition appropriate to the stackup and signal energy. Do not route fast nets over plane voids, connector keepouts, split references, or deep antipads without explicit analysis.
|
|
18
|
+
|
|
19
|
+
For differential pairs, preserve the same reference environment and avoid gratuitous uncoupling. Include package, connector, via, and pin-escape skew in the budget. Visual spacing does not prove differential impedance.
|
|
20
|
+
|
|
21
|
+
Count every via transition. At high edge rates, barrel, unused stub, antipad, return transition, and connector launch can dominate a short trace.
|
|
22
|
+
|
|
23
|
+
For memory, use the controller and memory-vendor topology. Address/command, clock, data, strobe, and mask groups have different timing relationships. Do not force one global length target.
|
|
24
|
+
|
|
25
|
+
For USB and Ethernet, follow the transceiver/connector reference design for termination, magnetics, and protection placement. Keep pair symmetry and return continuity.
|
|
26
|
+
|
|
27
|
+
Source geometry can prove routing choices. It cannot alone prove insertion loss, eye margin, impedance, or emissions.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# KiCad Native Evidence
|
|
2
|
+
|
|
3
|
+
Prefer native .kicad_sch and .kicad_pcb sources over screenshots.
|
|
4
|
+
|
|
5
|
+
Run:
|
|
6
|
+
python scripts/kicad_pcb_probe.py path/to/board.kicad_pcb > board-probe.json
|
|
7
|
+
|
|
8
|
+
The probe does not execute the board. It reports SHA-256, file size, KiCad format identity, layers, net names, footprint/segment/via/zone counts, segment-width range, and via geometry range.
|
|
9
|
+
|
|
10
|
+
Example constraint contract:
|
|
11
|
+
{
|
|
12
|
+
"required_layers": ["F.Cu", "B.Cu"],
|
|
13
|
+
"min_copper_layers": 2,
|
|
14
|
+
"min_track_width_mm": 0.15,
|
|
15
|
+
"min_via_diameter_mm": 0.45,
|
|
16
|
+
"min_via_drill_mm": 0.20,
|
|
17
|
+
"required_nets": ["GND"],
|
|
18
|
+
"min_footprints": 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Run:
|
|
22
|
+
python scripts/pcb_constraint_gate.py board-probe.json board-contract.json
|
|
23
|
+
|
|
24
|
+
The gate fails only on implemented explicit rules. It cannot infer missing engineering constraints.
|
|
25
|
+
|
|
26
|
+
When kicad-cli is installed, record its exact version first and use the commands supported by that installed release for ERC, DRC, and exports. Preserve exit status and output. Do not guess command switches across versions.
|
|
27
|
+
|
|
28
|
+
Probe PASS plus constraint-gate PASS is not equivalent to KiCad DRC PASS. Keep evidence classes separate.
|
|
29
|
+
|
|
30
|
+
Visual inspection is useful for placement, return-path discontinuities, reference transitions, neck-downs, thermal paths, connector mechanics, and markings, but it cannot override parser or DRC evidence.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse, hashlib, json, math, sys
|
|
3
|
+
|
|
4
|
+
def tokenize(text):
|
|
5
|
+
i, n = 0, len(text)
|
|
6
|
+
while i < n:
|
|
7
|
+
c = text[i]
|
|
8
|
+
if c.isspace():
|
|
9
|
+
i += 1
|
|
10
|
+
continue
|
|
11
|
+
if c in "()":
|
|
12
|
+
yield c
|
|
13
|
+
i += 1
|
|
14
|
+
continue
|
|
15
|
+
if c == '"':
|
|
16
|
+
i += 1
|
|
17
|
+
out = []
|
|
18
|
+
while i < n:
|
|
19
|
+
c = text[i]
|
|
20
|
+
if c == '"':
|
|
21
|
+
i += 1
|
|
22
|
+
break
|
|
23
|
+
if c == "\\" and i + 1 < n:
|
|
24
|
+
out.append(text[i + 1])
|
|
25
|
+
i += 2
|
|
26
|
+
else:
|
|
27
|
+
out.append(c)
|
|
28
|
+
i += 1
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError("unterminated string")
|
|
31
|
+
yield ("ATOM", "".join(out))
|
|
32
|
+
continue
|
|
33
|
+
j = i
|
|
34
|
+
while j < n and (not text[j].isspace()) and text[j] not in "()":
|
|
35
|
+
j += 1
|
|
36
|
+
yield ("ATOM", text[i:j])
|
|
37
|
+
i = j
|
|
38
|
+
|
|
39
|
+
def parse(text):
|
|
40
|
+
stack, root = [], None
|
|
41
|
+
for tok in tokenize(text):
|
|
42
|
+
if tok == "(":
|
|
43
|
+
node = []
|
|
44
|
+
if stack:
|
|
45
|
+
stack[-1].append(node)
|
|
46
|
+
stack.append(node)
|
|
47
|
+
elif tok == ")":
|
|
48
|
+
if not stack:
|
|
49
|
+
raise ValueError("unexpected close")
|
|
50
|
+
node = stack.pop()
|
|
51
|
+
if not stack:
|
|
52
|
+
if root is not None:
|
|
53
|
+
raise ValueError("multiple top-level forms")
|
|
54
|
+
root = node
|
|
55
|
+
else:
|
|
56
|
+
if not stack:
|
|
57
|
+
raise ValueError("atom outside list")
|
|
58
|
+
stack[-1].append(tok[1])
|
|
59
|
+
if stack:
|
|
60
|
+
raise ValueError("unclosed list")
|
|
61
|
+
if root is None:
|
|
62
|
+
raise ValueError("empty file")
|
|
63
|
+
return root
|
|
64
|
+
|
|
65
|
+
def head(node):
|
|
66
|
+
return node[0] if isinstance(node, list) and node and isinstance(node[0], str) else None
|
|
67
|
+
|
|
68
|
+
def child(node, name):
|
|
69
|
+
if not isinstance(node, list):
|
|
70
|
+
return None
|
|
71
|
+
for x in node[1:]:
|
|
72
|
+
if isinstance(x, list) and head(x) == name:
|
|
73
|
+
return x
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def scalar(node, name):
|
|
77
|
+
x = child(node, name)
|
|
78
|
+
return x[1] if x is not None and len(x) > 1 and isinstance(x[1], str) else None
|
|
79
|
+
|
|
80
|
+
def number(v):
|
|
81
|
+
try:
|
|
82
|
+
x = float(v)
|
|
83
|
+
return x if math.isfinite(x) else None
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def metric(values):
|
|
88
|
+
xs = sorted(x for x in values if x is not None)
|
|
89
|
+
if not xs:
|
|
90
|
+
return {"count": 0, "min": None, "max": None, "unique": []}
|
|
91
|
+
uniq = []
|
|
92
|
+
for x in xs:
|
|
93
|
+
if not uniq or abs(x - uniq[-1]) > 1e-12:
|
|
94
|
+
uniq.append(x)
|
|
95
|
+
return {"count": len(xs), "min": xs[0], "max": xs[-1], "unique": uniq[:128]}
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
ap = argparse.ArgumentParser(description="Deterministic structural probe for KiCad PCB files")
|
|
99
|
+
ap.add_argument("board")
|
|
100
|
+
ap.add_argument("--pretty", action="store_true")
|
|
101
|
+
ns = ap.parse_args()
|
|
102
|
+
data = open(ns.board, "rb").read()
|
|
103
|
+
try:
|
|
104
|
+
text = data.decode("utf-8")
|
|
105
|
+
except UnicodeDecodeError as e:
|
|
106
|
+
raise SystemExit("UTF-8 decode failed: %s" % e)
|
|
107
|
+
root = parse(text)
|
|
108
|
+
if head(root) != "kicad_pcb":
|
|
109
|
+
raise SystemExit("not a kicad_pcb root")
|
|
110
|
+
|
|
111
|
+
layers_node = child(root, "layers")
|
|
112
|
+
layers = []
|
|
113
|
+
if layers_node:
|
|
114
|
+
for x in layers_node[1:]:
|
|
115
|
+
if isinstance(x, list) and len(x) >= 3:
|
|
116
|
+
layers.append({"id": x[0], "name": x[1], "type": x[2]})
|
|
117
|
+
copper_layers = [x["name"] for x in layers if isinstance(x.get("name"), str) and x["name"].endswith(".Cu")]
|
|
118
|
+
|
|
119
|
+
nets, widths, via_sizes, via_drills = [], [], [], []
|
|
120
|
+
footprints = segments = vias = zones = 0
|
|
121
|
+
|
|
122
|
+
for x in root[1:]:
|
|
123
|
+
h = head(x)
|
|
124
|
+
if h == "net" and len(x) >= 3:
|
|
125
|
+
nets.append(str(x[2]))
|
|
126
|
+
elif h in ("footprint", "module"):
|
|
127
|
+
footprints += 1
|
|
128
|
+
elif h == "segment":
|
|
129
|
+
segments += 1
|
|
130
|
+
widths.append(number(scalar(x, "width")))
|
|
131
|
+
elif h == "via":
|
|
132
|
+
vias += 1
|
|
133
|
+
via_sizes.append(number(scalar(x, "size")))
|
|
134
|
+
d = child(x, "drill")
|
|
135
|
+
if d is not None and len(d) > 1:
|
|
136
|
+
via_drills.append(number(d[1]))
|
|
137
|
+
elif h == "zone":
|
|
138
|
+
zones += 1
|
|
139
|
+
|
|
140
|
+
result = {
|
|
141
|
+
"schema": "deadbyte.kicad-pcb-probe.v1",
|
|
142
|
+
"path": ns.board,
|
|
143
|
+
"sha256": hashlib.sha256(data).hexdigest(),
|
|
144
|
+
"bytes": len(data),
|
|
145
|
+
"kicad": {"version": scalar(root, "version"), "generator": scalar(root, "generator")},
|
|
146
|
+
"layers": layers,
|
|
147
|
+
"copper_layers": copper_layers,
|
|
148
|
+
"copper_layer_count": len(copper_layers),
|
|
149
|
+
"nets": sorted(set(nets)),
|
|
150
|
+
"counts": {"footprints": footprints, "segments": segments, "vias": vias, "zones": zones},
|
|
151
|
+
"track_width_mm": metric(widths),
|
|
152
|
+
"via_diameter_mm": metric(via_sizes),
|
|
153
|
+
"via_drill_mm": metric(via_drills)
|
|
154
|
+
}
|
|
155
|
+
json.dump(result, sys.stdout, indent=2 if ns.pretty else None, sort_keys=True)
|
|
156
|
+
sys.stdout.write("\n")
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|