Skip to content

fix(desktop): standard parts are told by SolidCraft's library and cannot be forged by a description - #356

Open
ManuelStaggl wants to merge 6 commits into
mainfrom
fix/standard-parts-audit
Open

ManuelStaggl wants to merge 6 commits into
mainfrom
fix/standard-parts-audit

Conversation

@ManuelStaggl

@ManuelStaggl ManuelStaggl commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the severity-A findings of the full audit (2026-11-12) on telling bought standard parts from printed bodies, and the gaps an independent review found in the first version of this change. Everything is measured with the audit harness (tools/audit, cases normteile-policy-*) against SolidCraft 0.40.1, headless.

How a standard part is told now

One recognition (boughtParts in mirror.ts) stands behind print check, plate, size list, export and the list of parts to buy. A body is a standard part only when

  1. its component's description is one parts.insert writes: a family's name, one of its sizes and a length, all read from parts.library (no list of names in PartBuddy);
  2. the component holds that one body and nothing else (all 13 families build exactly one);
  3. the body has the shape SolidCraft builds for that description: solidcraft-cli run builds the part alone, and both are measured with inspect.measure: volume and area, the counts of faces, edges and vertices, and of each face its area, the length of its outline and its radius (all within 1e-6).

Whatever cannot be read, measured or built counts as printed and is logged with the body and the reason: a screw in the print check shows, a printed part missing from it does not. The mirror does not wait for a part being built; export, slicer, size list and the parts to buy do.

S-01 / G2 / N-normteile-policy-1: one of 13 library families was printed and exported

before after
families recognised (one part each) 12 / 13 (countersunk_socket_screw missing) 13 / 13
every size of every family, plus odd lengths, a third turned to [1, 2, 3] not measured 185 / 185
partbuddy_mechanism {kind: screw, head: countersunk}: structure.standardParts [] [Body1, Body2]
bodies exported Block, Plate, Body1, Body2 Block, Plate
STL of the exported bodies 6084 triangles 1364 (Block and Plate alone: 1364)
control head: cap 2 standard parts, 1456 = 1456 unchanged

The tool's sentence "the screws are in the design as standard parts" is true for both heads. mechanismTool.ts and mechanisms.ts are untouched.

H-01: the assistant could turn a printed body into a "standard part"

execute component.metadata.set {component, description: "Hex nut M3"} took a 0.3 mm lid out of print check, plate and export. The policy refuses component.metadata.set with a description (description, values.description, as a batch step). Since review point 2 the forged description would no longer work either: the lid is not shaped like the nut.

before after
decide() allow deny
structure.standardParts after the attempt [Deckel] []
print check ready, "nothing found" block, "walls down to 0.3 mm"

Review

1. The policy compared command ids exactly; SolidCraft runs them in any case. Measured over 50 spellings of tool, command, argument and parameter names: SolidCraft takes a command id in any case (Component.Metadata.Set, File.Export), a batch step's command as id, and a batch's steps from a script file (path); tool names, argument names and parameter keys only as written (Execute, Params, Path, Key are unknown or ignored). Every rule now sees the lower-case, trimmed id, normalised in one place for execute, batch steps and nested steps; a step's id is decided like its command; batch with a path is refused. decide() for Component.Metadata.Set, Document.Metadata.Set with PartBuddy's key, Edit.Checkpoint, Doc.recovery_list, Diagnostics.Crash_Reports, a step {id: ...} and batch {path}: allow -> deny. Test: agent.test.ts, "decides a command id in any spelling SolidCraft runs".

2. Bodies in a library component counted unchecked. A box, a 0.3 mm lid (print check block) and an M3 nut; the steps sent past the policy:

steps before after
component.move_bodies of the lid into the nut ready, lid a standard part block, lid printed (2 bodies: stage 1)
component.copy_bodies into the nut reproduced by the review block (stage 1)
component.activate the nut + solid.box reproduced by the review block (stage 1)
solid.combine join of a plate onto the nut reproduced by the review block, nut body printed: 310.17 mm³ and 24 faces against 45.91 mm³ and 9 (stage 2)
solid.box {operation: join} onto the nut reproduced by the review block (stage 2)
solid.remove the nut, lid moved in not measured block (stage 2)
description "Hex nut M3" on the lid's component ready (H-01) block (stage 2)
nut described as "Hex nut M4" not measured nut printed (stage 2)
"Plain washer M3x8" on the lid, beside the nut not measured lid printed, nut still a standard part

Stage 2 comes from a reference, not from library dimensions: a world bounding box changes with the part's direction, volume and area do not (turned, hidden, resized: at most 6e-11 off the reference). What the reference costs and where it is kept: see the second review, point 1. Tests: structure.test.ts ("counts every body of a library component as printed…", "takes a body for the part only when it has the shape…"), partReference.test.ts, mirrorRevision.test.ts.

3. Body names kept in the design's metadata. The key is neither read nor written any more; parts put in from the menu are library components like any other. Measured: key set to ["SHCS M3x12"], nut renamed, lid renamed to SHCS M3x12: the lid stays printed, the renamed nut stays a standard part. The policy still refuses writes to the key by its prefix.

4. Parts to buy. No longer assembly.bom: the list is the standard parts themselves, counted by their placements through subassemblies, with the part number SolidCraft gives the reference. A nut, a washer in a subassembly placed twice, a printed lid: with the lid given part_number: "ISO 4032 M3" (BOM: one row, components [2, 5], count 2), the nut set to bom_type: reference (BOM: no nut), the nut's part number emptied: the list is 1 x Hex nut M3, 2 x Plain washer M3 in every case. Test: structure.test.ts, "lists the parts to buy from the standard parts themselves".

5. A failed parts.library switched the recognition off silently. A failed batch, a rejected request, no results and an empty library each write a line to the log; the library is read once per engine connection and again after a failure. Test: mirrorRevision.test.ts, "says so in the log when parts.library fails or is empty".

6. A fallback to the former twelve names is left out. A family SolidCraft renames or a size it drops cannot be built by the installed SolidCraft, so there is nothing to hold the body against; a fallback by name alone would be the gap of point 2 again. Such a part counts as printed and is logged.

Second review

1. The reference blocked the mirror, and a failed run was tried again with every mirror.

before after
what the mirror waits for a new part 0.37 s (1 part), 4.2 s (30), 43 s (all sizes), with every start 4 to 14 ms: it asks and goes on
build of the references in the mirror in the background: 0.7 s (1 part), 11 s (30), 98 to 112 s (all 185 sizes), once per SolidCraft program; then the design is mirrored once more
after a restart of the app built again 0.1 s, read from part-references.json in the cache folder (kept under the program's version, size and date)
a part SolidCraft refuses (Socket head cap screw M3x5000) one failed process with every mirror one run of 0.2 s, then remembered with its reason, over restarts too
a run that gives no answer up to (1 + N) x 2 min with every mirror ends after 20 s + 2 s a part (the slowest single part builds in 0.5 s); its parts are not tried again for 5 min

Until a part's reference is there its body counts as printed, so for that moment the print check sees the screw. What leaves PartBuddy does not: export, send to slicer, the size list and the parts to buy read the standard parts anew and wait for their references. The build takes two runs now (how many faces a part has is known only once it is built), which is why it is slower than the one the review timed.

2. The same reference was built twice by the mirror and the list of parts to buy. A description being built is not started again; whoever waits, waits for the run that is under way.

3. Five numbers did not fix the shape. A ring of R 4.0251, r 3.3699, 1 mm high has the volume, area and counts of the M3 washer to 2.6e-11. The shape now also holds each face's area, outline and radius. model.faces and model.edges were measured and are not used: they give values of the display mesh, up to 2 % apart between the same part straight and turned; inspect.measure of a face is exact. Measured: the ring, described as "Plain washer M3", stays printed; all 185 sizes stay standard parts, a third of them turned.

4. cliReferences was untested. partReference.test.ts, with the program's runs injected: a reference is kept and no process runs for it again; a refused part is remembered, also over a restart; a part that cannot be built lets the others stand; a run without an answer is tried again only after a while; the script file is removed; a background build asks for exactly one more mirror (mirrorRevision.test.ts: "mirrors the delivered revision again, once however often it is asked"); two askers, one build; the ring against the washer.

5. Older designs. No saved design with standard parts lies in the repository (apps/desktop/e2e, tools/fixtures, tools/templates, site: none). Measured instead on the two designs the audit saved on 2026-11-12 (families.sc, one part of each family; screw-cap.sc, a screw joint), opened again by solidcraft-cli run --in: 13 of 13 and 2 of 2 parts are told after the rebuild. Which SolidCraft wrote them (0.40.0-rc.4 or 0.40.1) the files do not say.

Still open

  • For the moment a new part's reference is built (0.7 s once per part and SolidCraft program; 0.1 s after each start of the app, until the kept references are read) its body is in the print check, and a note to the assistant taken in that moment would name it. Nothing is exported in that state.
  • A shape is as strong as these measures: a body that meets volume, area, counts and every face's area, outline and radius of a library part within 1e-6 would pass. For the washer these fix the ring; no other body was tried.
  • A library component with two bodies counts as printed altogether, the library's body too (marked shortcut: in mirror.ts).
  • A real part whose description is changed in SolidCraft itself counts as printed.
  • N-normteile-policy-4 and -5 (B: screenshot {path}, compare_picture {reference}, document.metadata.set {properties}) and S-30 (C) are not part of this change.

Not checked

  • The contract tests (e2e/solidcraft.contract.spec.ts) and the app itself were not started. Two of them depend on the reference from solidcraft-cli inside the app ("standard part" on a seated SHCS M3x12, two standard parts after a screw joint); the same flows pass in the harness. One expectation changed: the screw joint test waits up to 30 s for the two "standard part" labels (they follow a mirror after the bodies show), as the other test already does.
  • Designs written by SolidCraft 0.30.1 or a 0.40.0-rc, with parts put in from the menu: none is at hand and no other SolidCraft is installed here. If such a part's description or shape differs, its body counts as printed and the log names it: body "<name>" is described as "<description>" but is not that part (…) or …, which SolidCraft did not build to hold it against (<reason>).
  • SolidCraft's window as the engine (not headless), and a part placed several times by one parts.insert.

🤖 Generated with Claude Code

ManuelStaggl and others added 5 commits November 12, 2026 14:48
… part, not 12 named families

Audit S-01, G2, N-normteile-policy-1. The mirror told parts.insert components by a fixed list of
12 family names; SolidCraft 0.40.1 has 13. The countersunk screws partbuddy_mechanism puts in went
to print check, plate and export as printed bodies while the tool said they were standard parts.

The mirror now reads parts.library with the components' metadata and takes a description for a
part when it is a family's name, one of its sizes and a length. A family SolidCraft adds is known
without a change here; a text that only begins like a family ("Hex nut holder") is no part.

Measured against SolidCraft 0.40.1 (tools/audit, normteile-policy): families recognised 12/13 ->
13/13 (185 of 185 over every size); screw joint with head countersunk: structure.standardParts
[] -> [Body1, Body2], STL of the exported bodies 6084 -> 1364 triangles (block and plate alone:
1364).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ard part

Audit H-01. PartBuddy tells the parts SolidCraft's library put in by their component's description,
and the assistant could write that field: component.metadata.set {description: "Hex nut M3"} on a
lid 0.3 mm thick took the lid out of print check, plate and export.

The policy now refuses component.metadata.set with a description, in both forms SolidCraft 0.40.1
takes (description, values.description) and as a batch step. Other properties, component.rename
and free properties stay allowed; measured, none of them makes a body a standard part.

Measured (tools/audit, normteile-policy-metadata): decide() allow -> deny; after the attempt
structure.standardParts [Deckel] -> [], printed bodies [Box] -> [Box, Deckel], print check ready
-> block ("walls down to 0.3 mm").

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…t of printing

Audit S-01 (same recognition for every caller). The to-buy list of the sharing package took every
row of assembly.bom with a part number, whatever print check, plate and export made of its bodies.
A printed component with a part number was listed to print and to buy.

The list now keeps the rows whose components hold standard parts only, by the same
structure.standardParts the export leaves out.

Measured against SolidCraft 0.40.1: screw joint with two countersunk screws and a printed lid given
part_number "ISO 4032 M3": rows listed 2 (2 x ISO 10642 M3x10, 1 x ISO 4032 M3) -> 1 (2 x ISO 10642
M3x10); the lid stays in print check (block) and export.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…SolidCraft runs it

Review of the audit fix for H-01. The policy compared command ids exactly, SolidCraft reads them
in any case: execute Component.Metadata.Set, Document.Metadata.Set with PartBuddy's own key,
Edit.Checkpoint, Doc.recovery_list, Diagnostics.Crash_Reports and Settings.* were allowed and ran.
A batch step named by id instead of command, and a batch reading its steps from a script file
(path), passed every rule unseen.

Every rule now sees the lower-case, trimmed id, normalised in one place for execute, batch steps
and nested steps; a step's id is decided like its command; batch with a path is refused.

Measured with SolidCraft 0.40.1 (headless): of 50 spellings of tool, command, argument and
parameter names it runs a command id in any case, a step's id, and batch path; tool names,
argument names and parameter keys only as written. decide() for those forms: allow -> deny; the
description of the printed lid stays unset.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…hat very body for its part

Review of the audit fix for S-01 and H-01. A component described as a library part made every body
in it a standard part, so a printed lid moved, copied or built into a nut's component, or joined
onto the nut, left print check, plate and export. A second channel, body names kept in the
design's metadata, took a lid renamed to a remembered name. The list of parts to buy followed
assembly.bom, whose rows the assistant can merge or empty through part number and BOM type. A
failed parts.library switched the recognition off without a word.

One recognition now, behind print check, plate, size list, export and the list to buy:
- a library component counts only with the one body the library builds (all 13 families build one);
- that body must have the shape SolidCraft builds for the description: solidcraft-cli builds the
  part alone and both are measured with inspect.measure (volume, area, faces, edges, vertices);
- the names in the design's metadata are neither read nor written any more;
- the parts to buy are these parts, counted by their placements, with SolidCraft's part number;
- parts.library is read once per engine; whatever fails is logged and counts as printed.

Measured with SolidCraft 0.40.1 (tools/audit): lid 0.3 mm and an M3 nut, print check before the
steps "block". After move_bodies, copy_bodies, activate + box, combine join, box join, the nut
removed and the lid moved in, a forged description, a rename into the remembered name: "ready"
without the lid -> "block" with the lid in print check and export, in all 10 cases. Parts to buy
with the lid given the nut's part number, the nut set to reference, the nut's part number
emptied: nut missing or lid listed -> 1 nut, 2 washers in all cases. All 13 families and 185 of
185 sizes stay standard parts, a third of them turned; a part turned, hidden or resized differs
from its reference by at most 6e-11.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@ManuelStaggl
ManuelStaggl force-pushed the fix/standard-parts-audit branch from 93206cc to 9f88b5d Compare November 12, 2026 14:16
…ept, and told by every face

Second review of the audit fix for S-01 and H-01.

The mirror waited for solidcraft-cli to build each new part (0.4 s for one, 4 s for 30, 43 s for
all sizes, with every start of the app), a failed run was tried again with every mirror, and
volume, area and three counts did not fix the shape: a ring of other radii and height met the M3
washer on all five to 2.6e-11.

- The mirror no longer waits. A part without a reference counts as printed until it is built in
  the background; then the design is mirrored once more. Export, slicer, size list and the parts
  to buy wait for the references, so nothing leaves with a screw in it.
- References are kept in the cache folder under the program's version, size and date. A part
  SolidCraft does not build is remembered with its reason; a run that gave no answer is not
  tried again for five minutes. A run ends after 20 s plus 2 s a part instead of 2 min.
- A part asked for while it is being built is not built a second time.
- A shape is also the area, outline and radius of each face, from inspect.measure of the face.
  model.faces and model.edges give mesh values, up to 2 % apart between a part straight and
  turned.
- Every body left printed is logged with its name and the reason.

Measured with SolidCraft 0.40.1 (tools/audit): the mirror's question for references 4 to 14 ms
(before: 0.37 s to 43 s); background build 0.7 s for one part, 11 s for 30, 98 to 112 s for all
185 sizes, once per SolidCraft; after a restart 0.1 s from the file. A refused part (M3x5000):
one run of 0.2 s, then none (before: one with every mirror). The ring is printed; 13 of 13
families and 185 of 185 sizes stay standard parts, a third of them turned. Two designs saved by
the audit and opened again: 13 of 13 and 2 of 2 parts told.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant