Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import path from 'node:path';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { loadConfig } from '../config.js';
import { checkDrift } from '../memory/drift.js';
import { loadConstraints, checkForbiddenPins } from '../memory/constraints.js';
import { pinNets } from '../kicad/sexp.js';
import { openspecValidate } from '../openspec/cli.js';
import { runAgentLoop } from '../agent/loop.js';
import type { RunMetaInput } from '../agent/runmeta.js';
import type { ProgressRenderer } from '../agent/render.js';
/**
* `copperhead sync` (design D14): deterministic verify phase, then an optional
* spec-gated resolve phase. Truth precedence: KiCad files are ground truth for
* as-built facts; specs/budgets for requirements. Requirement violations are
* flagged, never auto-resolved.
*/
export interface SyncItem {
kind: 'drift' | 'dual-write' | 'pins-h' | 'coverage' | 'openspec';
doc: string;
claim: string;
actual: string;
resolution: string;
}
export interface SyncViolationItem {
kind: 'requirement-violation';
description: string;
governedBy: string;
}
export interface SyncReport {
resolvable: SyncItem[];
violations: SyncViolationItem[];
}
export async function syncVerify(repoRoot: string): Promise<SyncReport> {
const config = await loadConfig(repoRoot);
const resolvable: SyncItem[] = [];
const violations: SyncViolationItem[] = [];
if (config.schematic && existsSync(path.join(repoRoot, config.schematic))) {
for (const m of await checkDrift(repoRoot, config.docs, config.schematic)) {
resolvable.push({
kind: 'drift',
doc: m.doc,
claim: m.claim,
actual: m.actual,
resolution: `update ${m.doc} to match the as-built schematic (KiCad files are truth for as-built facts)`,
});
}
}
// dual-write audit: every registry key should be mentioned in some doc, and
// every budget in config should exist in the registry
const registry = await loadConstraints(repoRoot);
const docsDir = path.join(repoRoot, config.docs);
let docsText = '';
for (const name of ['SPEC.md', 'BOM.md', 'PINOUT.md', 'SUBSYSTEMS.md', 'LAYOUT.md']) {
const p = path.join(docsDir, name);
if (existsSync(p)) docsText += `\n<<<${name}>>>\n` + (await readFile(p, 'utf8'));
}
for (const key of Object.keys(registry)) {
const shortKey = key.split('.').pop()!;
if (docsText && !docsText.includes(shortKey)) {
resolvable.push({
kind: 'dual-write',
doc: 'constraints.json',
claim: `constraint ${key} exists in registry`,
actual: 'no doc mentions it',
resolution: `add the constraint to the doc named by its source (${registry[key]!.source})`,
});
}
}
for (const [budget, value] of Object.entries(config.budgets)) {
if (!Object.keys(registry).some((k) => k.endsWith(budget))) {
resolvable.push({
kind: 'dual-write',
doc: '.copperhead/config.json',
claim: `budget ${budget}=${value} configured`,
actual: 'not in constraints.json',
resolution: 'record_constraint with the budget value and its source',
});
}
}
// PINOUT.md vs generated pins.h
const pinsH = path.join(repoRoot, 'firmware', 'src', 'pins.h');
if (existsSync(pinsH) && existsSync(path.join(docsDir, 'PINOUT.md'))) {
const header = await readFile(pinsH, 'utf8');
const pinout = await readFile(path.join(docsDir, 'PINOUT.md'), 'utf8');
for (const m of header.matchAll(/#define\s+PIN_(\w+)\s+(\S+)/g)) {
if (!pinout.includes(m[1]!)) {
resolvable.push({
kind: 'pins-h',
doc: 'firmware/src/pins.h',
claim: `defines PIN_${m[1]}`,
actual: 'PINOUT.md does not mention it',
resolution: 'regenerate pins.h from PINOUT.md (PINOUT.md is the single source of truth)',
});
}
}
}
// coverage: docs the transparency layer expects
for (const name of ['DECISIONS.md', 'CHANGELOG.md']) {
if (!existsSync(path.join(docsDir, name))) {
resolvable.push({
kind: 'coverage',
doc: `${config.docs}${name}`,
claim: 'exists (transparency layer)',
actual: 'missing',
resolution: 'scaffold it (copperhead init creates it)',
});
}
}
if (existsSync(path.join(repoRoot, 'openspec', 'config.yaml'))) {
const res = await openspecValidate(repoRoot);
if (!res.ok) {
resolvable.push({
kind: 'openspec',
doc: 'openspec/',
claim: 'specs and changes validate',
actual: res.output.split('\n')[0] ?? 'validation failed',
resolution: 'fix the reported spec/change issues',
});
}
}
// requirement violations: never auto-resolved (AC-7.3)
if (config.schematic && existsSync(path.join(repoRoot, config.schematic))) {
const pins = await pinNets(path.join(repoRoot, config.schematic));
for (const v of checkForbiddenPins(registry, pins)) {
violations.push({
kind: 'requirement-violation',
description: v.description,
governedBy: v.source,
});
}
}
return { resolvable, violations };
}
export function formatSyncReport(report: SyncReport): string {
const lines: string[] = [];
if (!report.resolvable.length && !report.violations.length) {
return 'sync: no inconsistencies';
}
if (report.resolvable.length) {
lines.push(`${report.resolvable.length} resolvable inconsistency(ies):`);
for (const i of report.resolvable) {
lines.push(`- [${i.kind}] ${i.doc}: claims "${i.claim}" but actual is "${i.actual}"`);
lines.push(` resolution: ${i.resolution}`);
}
}
if (report.violations.length) {
lines.push(`${report.violations.length} REQUIREMENT VIOLATION(S) (not auto-resolved; human decision needed):`);
for (const v of report.violations) {
lines.push(`- ${v.description}`);
lines.push(` governed by: ${v.governedBy}`);
}
}
return lines.join('\n');
}
export async function syncResolve(
repoRoot: string,
report: SyncReport,
model: string,
log: (s: string) => void,
extras?: { renderer?: ProgressRenderer; meta?: RunMetaInput },
): Promise<{ ok: boolean }> {
const reportText = formatSyncReport(report);
const res = await runAgentLoop({
repoRoot,
model,
request: 'resolve design-state inconsistencies found by copperhead sync',
stagePrompt: `You are resolving drift found by the deterministic sync verifier. The inconsistency report is below. Truth precedence: the KiCad files are ground truth for as-built facts (fix the docs to match); openspec specs and SPEC.md budgets are ground truth for requirements. Do NOT touch anything listed as a requirement violation; those are for the human. Apply each proposed resolution, verify, and finish.\n\n${reportText}`,
log,
...(extras?.renderer ? { renderer: extras.renderer } : {}),
...(extras?.meta ? { meta: extras.meta } : {}),
});
return { ok: res.outcome === 'success' };
}
7f2688d19This file is pinned to the selected release. Download the original to open it in your design tool.