Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
/**
* Machine-readable constraint registry (SPEC §2.6). Built simultaneously with
* the docs: every stated/assumed/discovered constraint lands in both in the
* same tool turn. `affects` drives propagation.
*/
export interface Constraint {
min?: number;
max?: number;
forbidden?: string[];
value?: string | number;
source: string;
affects: string[];
/**
* Subset of `affects` whose target artifact (schematic/board/BOM) did not
* exist when the constraint was recorded. No revisit obligation is open for
* these; they re-open at the start of the first run where the artifact
* exists (reopenDeferredAffects), then the marker is removed.
*/
deferred?: string[];
}
export type ConstraintRegistry = Record<string, Constraint>;
export function constraintsPath(repoRoot: string): string {
return path.join(repoRoot, '.copperhead', 'constraints.json');
}
export async function loadConstraints(repoRoot: string): Promise<ConstraintRegistry> {
const p = constraintsPath(repoRoot);
if (!existsSync(p)) return {};
return JSON.parse(await readFile(p, 'utf8')) as ConstraintRegistry;
}
export async function saveConstraints(repoRoot: string, registry: ConstraintRegistry): Promise<void> {
const p = constraintsPath(repoRoot);
await mkdir(path.dirname(p), { recursive: true });
await writeFile(p, JSON.stringify(registry, null, 2) + '\n', 'utf8');
}
export async function saveConstraint(
repoRoot: string,
key: string,
constraint: Constraint,
): Promise<ConstraintRegistry> {
const registry = await loadConstraints(repoRoot);
registry[key] = constraint;
await saveConstraints(repoRoot, registry);
return registry;
}
/** The build artifact an `affects` item names, when it clearly names one. */
export type AffectsTarget = 'schematic' | 'board' | 'bom';
/**
* Items that name a not-yet-built artifact are deferrable; items that name a
* specific refdes, net, or doc fact return null and open a revisit obligation
* immediately. Misclassification is cheap in both directions: an unmatched
* artifact item just costs one ceremonial resolve_affected call (the old
* behavior for everything), and a deferred item still re-opens later.
*/
export function classifyAffectsTarget(item: string): AffectsTarget | null {
if (/\b(bom|part[-\s]?selection|mpn)\b/i.test(item)) return 'bom';
if (/\b(schematic|pinout|pin[-\s]?assign\w*|strapping|netlist)\b/i.test(item)) return 'schematic';
if (
/\b(layout|stackup|rout(?:e|es|ing)?|vias?|pours?|keepouts?|zones?|copper|traces?|board|pcb|mounting|thermal|silkscreen|assembly|current[-\s]?carrying)\b/i.test(
item,
)
)
return 'board';
return null;
}
interface ArtifactConfig {
schematic: string | null;
board: string | null;
docs: string;
}
export function affectsTargetExists(target: AffectsTarget, repoRoot: string, config: ArtifactConfig): boolean {
switch (target) {
case 'schematic':
return !!config.schematic;
case 'board':
return !!config.board;
case 'bom':
return existsSync(path.join(repoRoot, config.docs, 'BOM.md'));
}
}
export interface ReopenedAffects {
key: string;
item: string;
}
/**
* Run-start hook: re-open the revisit obligations that were deferred while
* their target artifact did not exist. Each re-opened item is removed from the
* registry's `deferred` marker in the same pass, so it re-opens exactly once —
* from then on it lives in the run's ledger like any other obligation.
*/
export async function reopenDeferredAffects(
repoRoot: string,
config: ArtifactConfig,
openObligation: (key: string, item: string) => void,
): Promise<ReopenedAffects[]> {
const registry = await loadConstraints(repoRoot);
const reopened: ReopenedAffects[] = [];
for (const [key, c] of Object.entries(registry)) {
if (!c.deferred?.length) continue;
const stillDeferred: string[] = [];
for (const item of c.deferred) {
const target = classifyAffectsTarget(item);
if (target && !affectsTargetExists(target, repoRoot, config)) {
stillDeferred.push(item);
continue;
}
openObligation(key, item);
reopened.push({ key, item });
}
if (stillDeferred.length) c.deferred = stillDeferred;
else delete c.deferred;
}
if (reopened.length) await saveConstraints(repoRoot, registry);
return reopened;
}
export interface ConstraintViolation {
key: string;
description: string;
source: string;
}
/**
* Mechanical validation where possible (SPEC §2.6): forbidden pins against the
* pinout, numeric budget keys surfaced for the doc-level checks. Geometry
* checks are out of scope for Phase 1.
*/
export function checkForbiddenPins(
registry: ConstraintRegistry,
pinNets: { ref: string; pinName: string; net: string | null }[],
): ConstraintViolation[] {
const violations: ConstraintViolation[] = [];
for (const [key, c] of Object.entries(registry)) {
if (!c.forbidden?.length) continue;
for (const pn of pinNets) {
if (pn.net && c.forbidden.includes(pn.pinName)) {
violations.push({
key,
description: `${pn.ref} pin ${pn.pinName} is connected to net ${pn.net} but is forbidden by ${key}`,
source: c.source,
});
}
}
}
return violations;
}
696b6f9f0This file is pinned to the selected release. Download the original to open it in your design tool.