design record: an authorship field, recorded and in neither id

The person is identified by an email address. A handle may be chosen later
and does not replace it: the handle is a display name, the address is the id.

Excluded from input_id and build_id. input_id answers whether two records
describe the same part as specified, so two people specifying the same part
must collide there; hashing the author would make identical parts claim to be
different designs. The guarantee is structural rather than careful -- the ids
are computed from the resolved parameter set and the author never enters it.

That exclusion narrows the one-way door it was scheduled against but does not
close it. A record regenerated later with the author filled in keeps its
input_id, but build_id covers the code revision and the Shapely and GEOS
versions, and boolean results on near-degenerate geometry can shift between
GEOS releases -- the F-034 mechanism. Regenerate after a GEOS bump and you have
attribution under a different build identity beside a printed part nobody can
tie to either. Recoverable, not free. Priority order in HANDOFF section 4 is
reversed accordingly: this before STL export, so the first coupon off the
printer carries its author.

The rendered line carries its own limit -- "(self-declared, unverified)".
Nothing checks that the address belongs to whoever typed it, and a bare email
in a field called author reads as identity to someone finding the file in five
years. The record is meant to outlive everyone present, so it states what it
knows. When an authenticated identity exists the qualifier changes and the
distinction stays legible. Same discipline as Provenance.describe(), a separate
type: provenance is about measurement, and design_record imports nothing from
mechcomp, which is what keeps a failure in the record off the build path.

parse() gets its own branch because authorship is neither input nor output and
inherits neither rule. It recovers the address and never the verification: a
text file cannot attest to its own verification, so reading a verified record
back as self-declared understates the claim, which is the only safe direction.

REVISION stays 8.0.0. The field reaches no geometry.

547 passed. The 529 are unchanged and no oracle case moved. The eighteen new
assertions were mutation-tested with PYTHONDONTWRITEBYTECODE=1 and caches
cleared between runs: leaking the author into input_canonical, deafening the
parser, trusting the adjective, dropping the qualifier, and turning an empty
author into an empty claim each fail the suite.
This commit is contained in:
2026-09-12 03:56:50 -05:00
parent a7162bc1d5
commit 48d566574e
5 changed files with 409 additions and 9 deletions
+28 -3
View File
@@ -14,6 +14,12 @@ WHAT IT SHOWS
what you are looking at is visible while you tune, not discovered
afterwards.
The author field is part of that. It is an email address, it is never
checked, and the record says so on the line it appears on. Nothing here
persists it: it lives in the page for as long as the page is open, because
remembering it across visits would be the first half of a persistence
layer that does not exist yet.
WHAT IT DOES NOT DO
It does not export STL, and it does not persist anything. It is a viewer and
a control panel over ``build()``. Rejections are shown as messages rather
@@ -95,7 +101,8 @@ def profile_params(family, profile: str) -> List[Tuple[str, List[str]]]:
def build_payload(family_name: str, profile: str,
overrides: Dict[str, Any]) -> Dict[str, Any]:
overrides: Dict[str, Any],
author: str = "") -> Dict[str, Any]:
from mechcomp.profiles import ProfileRejected, build
family = families()[family_name]
@@ -126,7 +133,8 @@ def build_payload(family_name: str, profile: str,
}
try:
result = build(family=family_name, profile=profile, params=typed)
result = build(family=family_name, profile=profile, params=typed,
author=author)
except ProfileRejected as exc:
payload["ok"] = False
payload["message"] = str(exc)
@@ -170,6 +178,8 @@ PAGE = """<!doctype html>
input,select { font:inherit; font-size:12.5px; padding:4px 7px;
border:1px solid var(--line); border-radius:5px; background:#fff; width:100%; }
input:focus,select:focus { outline:2px solid var(--accent); outline-offset:-1px; }
label.wide { grid-template-columns:1fr; gap:3px; }
.hint { font-size:11px; color:#7b8794; margin:-2px 0 10px; }
.figure { background:#fff; border:1px solid var(--line); border-radius:9px;
padding:18px; display:inline-block; min-width:300px; min-height:200px; }
.bad { background:#fff4ed; border:1px solid #f0c3a2; border-radius:9px;
@@ -198,6 +208,13 @@ PAGE = """<!doctype html>
<label><span>family</span><select id="family"></select></label>
<label><span>profile</span><select id="profile"></select></label>
</fieldset>
<fieldset>
<legend>Author</legend>
<label class="wide"><span>email</span><input id="author" type="email"
autocomplete="email" placeholder="you@example.com"></label>
<p class="hint">Recorded with the design, never checked, and not stored
anywhere. The record says it is self-declared.</p>
</fieldset>
<div id="controls"></div>
</div>
<div class="stage">
@@ -220,6 +237,8 @@ async function refresh(sendValues) {
const q = new URLSearchParams();
q.set("family", state.family);
q.set("profile", state.profile);
const author = document.getElementById("author").value.trim();
if (author) q.set("author", author);
if (sendValues) for (const [k,v] of Object.entries(state.values)) q.set(k, v);
const data = await (await fetch("/api/build?" + q.toString())).json();
@@ -286,6 +305,8 @@ function applyToggles() {
for (const id of ["t-material","t-cavity","t-stock"])
document.getElementById(id).onchange = applyToggles;
document.getElementById("author").onchange = () => refresh(true);
refresh(false);
</script></body></html>
"""
@@ -311,8 +332,12 @@ class Handler(BaseHTTPRequestHandler):
q = {k: v[0] for k, v in parse_qs(parsed.query).items()}
family = q.pop("family", "3x")
profile = q.pop("profile", "Y")
# Popped rather than left in the mapping. Everything remaining is
# treated as a build parameter, and an author that survived into
# that mapping would be one rename away from reaching the ids.
author = q.pop("author", "")
try:
payload = build_payload(family, profile, q)
payload = build_payload(family, profile, q, author)
except Exception as exc: # noqa: BLE001
payload = {"ok": False,
"message": "%s: %s" % (type(exc).__name__, exc),