design record: what a model was made from, sufficient to regenerate it

A parametric compiler is meant to be tuned: print, measure the print, adjust, print again. That loop also destroys the value of everything already printed, because once a clearance moves the parts on the bench become unidentifiable and unreproducible. Configurability and reproducibility conflict unless something records what each part was made from.

The record carries the fully resolved parameter set at full precision, the stock and fit lines with whatever provenance exists, the code revision and the toolchain. Plain text, one fact per line, readable without this software.

It is deliberately not built from the report. geom.report rounds to six significant figures because that is what echo printed and the oracle records what was printed. A part cannot be regenerated from SECTION_AREA_MM2 equals 135.574. Inputs reproduce, outputs confirm: reported values are carried separately as verification, to be checked with a caliper against the actual part.

Two identities. input_id covers family, profile and parameters, the design intent. build_id adds code revision and toolchain. Shapely and GEOS are in build_id because boolean results on near degenerate geometry can shift between GEOS releases, which is the F-034 mechanism, and a record omitting them could not explain why the same numbers produced a different part. Timestamp, note and verification values are excluded from both.

Not on the build path. No profile imports it, so the frozen oracle cannot move. Twelve mutations tested and caught, including two defects of mine: the record carrying only overrides rather than the resolved set, and the toolchain never reaching build_id.
This commit is contained in:
2026-08-23 10:37:17 -05:00
parent 70787ea17d
commit d2827c23b3
2 changed files with 645 additions and 0 deletions
+367
View File
@@ -0,0 +1,367 @@
"""
The design record: what a model was made from, emitted with it.
WHY THIS EXISTS
The compiler is parametric, which means every dimension is an input at every
run and you are expected to tune them: print, measure the print, adjust,
print again. That loop is the tool working correctly.
It is also the loop that destroys the value of everything already printed.
Tune a clearance from 0.25 to 0.15 and the parts on the bench become
unidentifiable -- you cannot tell which setting produced which part, cannot
print a matching second one, and cannot reproduce the good one after the
settings have moved on. Configurability and reproducibility are in direct
conflict unless something records what each part was made from.
That something is this. A design record travels with a generated model and
carries the fully resolved input set at full precision, the stock and fit
descriptions with whatever provenance exists, the code that consumed them,
and an identity hash. It is the same discipline as the frozen oracle, turned
around to face the output instead of the reference.
WHAT IT IS NOT BUILT FROM
Not the report. ``geom.report`` rounds every number to six significant
figures because that is what OpenSCAD's ``echo`` printed and the oracle
records what was printed. Six figures is lossy: a part cannot be regenerated
from ``SECTION_AREA_MM2 = 135.574``.
What regenerates a model is its INPUTS -- family, profile, and the resolved
parameter set, stored at full precision with ``repr`` so they round-trip
exactly. Reported values are carried too, but as VERIFICATION: numbers a
caliper on the actual part can be checked against. Inputs reproduce;
outputs confirm.
TWO IDENTITIES, BECAUSE THEY ANSWER DIFFERENT QUESTIONS
``input_id`` the design intent: family, profile, parameters. Two records
with the same input_id describe the same part as specified.
``build_id`` input_id plus the code revision and toolchain. Two records
with the same build_id should produce identical bytes.
Keeping them apart matters. When the code changes and the input_id still
matches, you know the intent is unchanged and the geometry MIGHT have moved
-- which is exactly the moment to check, rather than to assume either way.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass, field
from typing import Dict, List, Mapping, Optional, Tuple
FORMAT_VERSION = "1"
# Reported values worth carrying for verification: the ones a person can put a
# caliper on, plus the counts that say the part is what it claims to be. Kept
# short deliberately -- a record nobody reads verifies nothing.
VERIFY_KEYS: Tuple[str, ...] = (
"ENVELOPE_X_MM",
"ENVELOPE_Y_MM",
"MIN_WALL_ACTUAL_MM",
"MIN_WALL_SPEC_MM",
"SECTION_PARTS",
"STRAP_CHANNELS",
"SECTION_AREA_MM2",
"MASS_G",
"LENGTH_MM",
)
# ---------------------------------------------------------------------------
# Canonical form
# ---------------------------------------------------------------------------
def canonical_value(value: object) -> str:
"""
A value's exact textual form.
``repr`` on a float round-trips exactly in Python 3, which is the whole
point: ``float(repr(x)) == x`` for every finite x, so a record can be read
back and rebuilt without drift. ``str`` and ``%g`` both lose bits and would
make the record a description of the part rather than a recipe for it.
"""
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, float):
return repr(value)
if isinstance(value, int):
return repr(value)
if isinstance(value, (list, tuple)):
return "[%s]" % ", ".join(canonical_value(v) for v in value)
return str(value)
def parse_value(text: str) -> object:
"""Inverse of ``canonical_value`` for the scalar cases a parameter can hold."""
if text == "true":
return True
if text == "false":
return False
try:
return int(text)
except ValueError:
pass
try:
return float(text)
except ValueError:
pass
return text
def canonical_params(params: Mapping[str, object]) -> str:
"""
Sorted ``key=value`` lines. Sorted so that dict ordering -- which depends on
how a caller happened to build the mapping -- cannot change the identity of
a design that is otherwise the same.
"""
return "\n".join("%s=%s" % (k, canonical_value(params[k]))
for k in sorted(params))
def resolve(defaults: Mapping[str, object],
params: Optional[Mapping[str, object]] = None) -> Dict[str, object]:
"""
The resolved parameter set, exactly as ``_common.assemble`` computes it.
``params`` carries only a case's overrides; everything else comes from the
family defaults. Reproducing that merge here rather than importing it keeps
this module off the build path -- nothing in a profile depends on the design
record existing -- at the cost of one line that must stay in step. A test
asserts it does.
"""
return {**dict(defaults), **dict(params or {})}
# ---------------------------------------------------------------------------
# The record
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class DesignRecord:
"""
Everything needed to identify, verify and regenerate one model.
``generated`` is deliberately excluded from both hashes. A record made today
and the same record remade tomorrow describe the same part, and if the
timestamp entered the identity then regenerating a design would always
appear to produce a different one.
"""
family: str
profile: str
params: Dict[str, object]
code_revision: str = "unknown"
generator_revision: str = "unknown"
toolchain: Dict[str, str] = field(default_factory=dict)
stock: List[str] = field(default_factory=list)
fit: List[str] = field(default_factory=list)
verification: Dict[str, object] = field(default_factory=dict)
generated: str = ""
note: str = ""
# -- identity ----------------------------------------------------------
@property
def input_canonical(self) -> str:
return "\n".join([
"format=%s" % FORMAT_VERSION,
"family=%s" % self.family,
"profile=%s" % self.profile,
"params:",
canonical_params(self.params),
])
@property
def build_canonical(self) -> str:
tools = "\n".join("%s=%s" % (k, self.toolchain[k])
for k in sorted(self.toolchain))
return "\n".join([
self.input_canonical,
"code_revision=%s" % self.code_revision,
"generator_revision=%s" % self.generator_revision,
"toolchain:",
tools,
])
@property
def input_id(self) -> str:
return hashlib.sha256(self.input_canonical.encode("utf-8")).hexdigest()[:16]
@property
def build_id(self) -> str:
return hashlib.sha256(self.build_canonical.encode("utf-8")).hexdigest()[:16]
# -- rendering ---------------------------------------------------------
def render(self) -> str:
"""
The record as it ships beside a model.
Plain text, one fact per line, readable without this software. A record
that can only be opened by the program that wrote it is not much of a
record -- the point is that it still means something in five years, on a
machine where none of this is installed.
"""
lines: List[str] = [
"MECHCOMP DESIGN RECORD %s" % FORMAT_VERSION,
"",
"input_id %s design intent: family, profile, parameters"
% self.input_id,
"build_id %s intent plus the code and toolchain that consumed it"
% self.build_id,
"",
"family %s" % self.family,
"profile %s" % self.profile,
]
if self.generated:
lines.append("generated %s (excluded from both ids)" % self.generated)
if self.note:
lines.append("note %s" % self.note)
lines += ["", "-- stock ------------------------------------------------"]
lines += [" %s" % s for s in (self.stock or ["(none recorded)"])]
lines += ["", "-- fit --------------------------------------------------"]
lines += [" %s" % f for f in (self.fit or ["(none recorded)"])]
lines += ["", "-- parameters, full precision, sufficient to regenerate --"]
for k in sorted(self.params):
lines.append(" %s=%s" % (k, canonical_value(self.params[k])))
lines += ["", "-- build ------------------------------------------------",
" code_revision=%s" % self.code_revision,
" generator_revision=%s" % self.generator_revision]
for k in sorted(self.toolchain):
lines.append(" %s=%s" % (k, self.toolchain[k]))
if self.verification:
lines += ["", "-- verification: measure the print against these -------"]
for k in sorted(self.verification):
lines.append(" %s=%s" % (k, canonical_value(self.verification[k])))
lines += ["",
" These are the compiler's own reported values, rounded to six",
" significant figures as the report emits them. They confirm a",
" part; they do not regenerate one. The parameters above do."]
return "\n".join(lines) + "\n"
# -- reading back ------------------------------------------------------
@staticmethod
def parse(text: str) -> "DesignRecord":
"""
Read a rendered record back.
Only the fields that reproduce a design are recovered -- family, profile,
parameters, build identity. Verification values are outputs and are
deliberately not fed back in as inputs.
"""
family = profile = ""
code_revision = generator_revision = "unknown"
params: Dict[str, object] = {}
toolchain: Dict[str, str] = {}
section = ""
for raw in text.splitlines():
line = raw.rstrip()
if line.startswith("-- parameters"):
section = "params"
continue
if line.startswith("-- build"):
section = "build"
continue
if line.startswith("-- "):
section = ""
continue
if section == "params" and line.startswith(" ") and "=" in line:
key, _, value = line.strip().partition("=")
params[key] = parse_value(value)
elif section == "build" and line.startswith(" ") and "=" in line:
key, _, value = line.strip().partition("=")
if key == "code_revision":
code_revision = value
elif key == "generator_revision":
generator_revision = value
else:
toolchain[key] = value
elif line.startswith("family "):
family = line.split(None, 1)[1].strip()
elif line.startswith("profile "):
profile = line.split(None, 1)[1].strip()
return DesignRecord(
family=family, profile=profile, params=params,
code_revision=code_revision,
generator_revision=generator_revision,
toolchain=toolchain,
)
# ---------------------------------------------------------------------------
# Construction from a build
# ---------------------------------------------------------------------------
def toolchain_versions() -> Dict[str, str]:
"""
Versions that can change emitted geometry.
Shapely and GEOS are here because HANDOFF section 5 records that boolean
results on near-degenerate geometry can shift between GEOS releases -- which
is precisely the F-034 mechanism. A record that omits them cannot explain why
the same parameters produced a different part.
"""
out: Dict[str, str] = {}
try:
import shapely
out["shapely"] = shapely.__version__
try:
out["geos"] = shapely.geos_version_string
except AttributeError:
pass
except Exception: # noqa: BLE001
out["shapely"] = "unavailable"
try:
import numpy
out["numpy"] = numpy.__version__
except Exception: # noqa: BLE001
pass
return out
def record_for(family: str, profile: str,
defaults: Mapping[str, object],
params: Optional[Mapping[str, object]] = None,
report: Optional[Mapping[str, object]] = None,
stock: Optional[List[str]] = None,
fit: Optional[List[str]] = None,
code_revision: str = "unknown",
generator_revision: str = "unknown",
generated: str = "",
note: str = "") -> DesignRecord:
"""
Build a record for one design.
``defaults`` comes from the family (``three_x.FAMILY.defaults``), ``params``
is the override set, and the merge reproduces what ``assemble`` does.
"""
verification: Dict[str, object] = {}
if report:
verification = {k: report[k] for k in VERIFY_KEYS if k in report}
return DesignRecord(
family=family,
profile=profile,
params=resolve(defaults, params),
code_revision=code_revision,
generator_revision=generator_revision,
toolchain=toolchain_versions(),
stock=list(stock or []),
fit=list(fit or []),
verification=verification,
generated=generated,
note=note,
)