SCALE-SEAM (100) — the element-connection pair, and a destination named at its real strength - #433
Conversation
…trix loses
Moves `lodSummary`/`setLod` and `phasing`/`setPhase` out of `client.ts` into
`api/model.ts`. They answer one question — *what state are the model's elements
in, and set it?* — and `client.ts` goes 727 -> 711, 76 methods above the STAYING
banner.
The grouping is derived, not asserted:
- identical return shape `{ total, <x>ed, prop, counts: Record<...> }`;
- both writers are `(pid, guids, <enum>, publish) -> editIfc`;
- both readers are consumed by `viewer/tools/modelStatePanels.ts` (251, 316);
- both writers sit unwired and *adjacent* on `clientCallers.test.ts`'s UNCALLED
allowlist;
- `model.ts` already owned `/model/lod/census`, `/lod/handover-readiness` and
`/lod/assessment`, while the base distribution `/projects/{pid}/lod` was left
behind in `client.ts` — `lodSummary` was a sibling separated from its family.
`authoring_matrix.py` DISAGREES and is recorded as the losing vote rather than
elided. It files `set_lod` under `data` and `set_phase` under `lifecycle`,
because it categorises by the IFC output each recipe writes — an LOD stage tag
against `Massing_Phasing.Status`. Different property sets, same question, which
is (89)'s "storage is a HOW" trap. This is the first slice where the matrix has
been wrong after being right three running.
It also MEETS (94)'s objection rather than overriding it: that slice declined
`setPhase` because taking the writer alone would have stranded `phasing()`, the
reader/writer split (87) had to undo. Both halves move together here.
The four names are added to `surface.test.ts` because its floor is a slack
ratchet (788 actual vs 751 floor) — the count alone would not notice a loss —
and because the UNCALLED allowlist is about call sites, not the surface.
Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
`test_file_sizes.py` / `test_claude_md_gates.py` / `test_roadmap_status.py` /
ruff all 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…ness that actually bounds Moves `lod500`, `setManufacturerInfo` and `attachOmDocument` out of `client.ts` into `api/model.ts`, rejoining `verifyAsbuilt` and `recordAsbuiltDimension` which (94) moved. `client.ts` goes 711 -> 698, 73 methods above the STAYING banner. TWO BOUNDING WITNESSES, derived independently, agreeing: - `openAsBuiltPanel` in `viewer/tools/modelStatePanels.ts` calls exactly five API methods — read off that function's brace closure rather than grepped for, so it is the closure of a scope and not a sample. Two of the five were already in `model.ts`. - The reader's own response type names its writer set field by field: `verified`/`by_method` from `verify_asbuilt`; `with_dimensions`/`dimensions_out_of_tolerance` from `record_asbuilt_dimension`; `with_manufacturer`/`with_serial` from `set_manufacturer_info`; `with_om_docs`/`om_documents` from `attach_om_document`. The backend route says it in prose: "Stamp elements with the `verify_asbuilt` recipe." A reader whose response type enumerates its writers is a DERIVED population. After eight slices of sampling, that is the first grouping witness here that bounds a set instead of illustrating one. WHAT CORROBORATES IS NOT WHAT BOUNDS. `test_lod500.py` reaches exactly three recipes — `attach_om_document`, `set_manufacturer_info`, `verify_asbuilt` — and OMITS `record_asbuilt_dimension`, which is unambiguously in this family. It agrees with the answer without being able to establish it. Three slices claimed "and no others" off a test file and were wrong all three times. NOT CLAIMED, and said in the header, the pin, the changelog and the roadmap rather than only in review: `attachDocument` stays in `client.ts`, takes a `purpose` parameter, and `asbuilt_summary` counts ANY purpose-tagged document reference — so "every writer of `with_om_docs` moves here" is false. The field map is of the recipes each field was designed around, not of everything that can set it. Two sources disagree and lose for the same reason: `attach_om_document` is a purpose-tagged wrapper of `detailing.attach_document`, and `authoring_matrix.py` files it and `set_manufacturer_info` under `data`. The first is a shared HELPER, the second a STORAGE bucket — the groupings (89) and (90) each had to reject. The matrix is the losing vote twice running after being right three times. Also fixes the extraction helper: it terminated its brace count on the method signature line, so a signature wrapping across two lines with no opening brace on the first read as a 3-line method. `attachOmDocument` is 6. Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0, ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`, `test_claude_md_gates.py`, `test_roadmap_status.py`, `test_ruff_scope.py`, `test_reachable.py` all 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #412 was squash-merged, so origin/main carries (95) as b0e8281 while this branch still pointed at the pre-squash 44a6ed4. The content diff between the remote branch and origin/main was empty before this merge, so the branch held no unmerged work; -s ours keeps this branch's tree (origin/main plus (96)) and reconciles the histories without rewriting the remote branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
… and was not
Moves `editHistory`, `editUndo` and `editRedo` out of `client.ts` into
`api/authoring.ts`. `client.ts` goes 698 -> 687, 70 methods above the STAYING
banner.
WHY THAT MIXIN: `editIfc`, already there, is the PUSH they pop. `authoring.py`
records the pre-edit version on every `/edit` call "so this edit can be undone",
`_restore_version` pops that stack, and `edit_history.state()` reads its depths.
One stack, and the operation that fills it was already in this file.
The types agree: both writers return `{restored, state: {can_undo, can_redo}}`,
and `state` is `editHistory`'s own return type minus the depths — the writers
hand back the reader's answer. That is a type-level relation, not the shared
`/edit/` prefix. The prefix is real and is deliberately not the argument, since
a route prefix is exactly what the verification slice was caught grouping on.
A HYPOTHESIS TESTED AND WITHDRAWN, which is the part worth keeping. "Undo
restores the prior model version" makes `model.ts` the obvious home — it owns
`modelVersions`, `versionDiff`, `versionCostDelta`. It is the wrong home: those
read `/projects/{pid}/versions` out of `bim.py`, while undo pops a DIFFERENT
stack, the `edit_history` sidecar, which `recipe_log.py` describes as a list of
file paths with "No recipe, no parameters, no actor". Two stacks, one word — and
the word is what made the wrong answer look obvious. Second withdrawal of a
plausible destination after checking it, after (93) withdrew (92)'s `HttpCore`
forecast.
THE BOUND IS WEAKER THAN (96)'s AND IS STATED THAT WAY. `app.ts`'s S4 block
wires `refreshUndo` (calling only `editHistory`) and `doUndoRedo` (calling only
`editUndo`/`editRedo`), so the union is exactly these three — but that unit is a
block delimited by reading, not a closure the braces define. It corroborates; it
does not bound. Not every set has a witness as strong as the last one's, and
promoting a block to a closure would be this sequence's own recurring defect.
Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py` all 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #413 was squash-merged, so origin/main carries (96) as ffb7b9f while this branch still held the pre-squash commits. The content diff between the remote branch and origin/main was empty before this merge, so the branch held no unmerged work; -s ours keeps this branch's tree (origin/main plus (97)) and reconciles the histories without rewriting the remote branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…flagged one CodeRabbit flagged `editUndo`'s docstring on #414: it says "restore the prior model version + republish" unconditionally, but `publish` defaults to true and can be false. Verified against the backend — `_restore_version` guards the republish with `if publish:` — so the claim is wrong whenever a caller passes `publish: false`. Grepping every site of that wording found the same unconditional claim in THREE live places, not one: - `apps/web/src/api/authoring.ts` — `editUndo` (the flagged one) - `services/api/src/aec_api/routers/authoring.py` — the `/edit/undo` route - `services/api/src/aec_api/routers/authoring.py` — the `/edit/redo` route All three now say republishing happens only when `publish`. The remaining hits are UI button labels, where the call site never passes the flag so republish really is unconditional, and historical CHANGELOG entries; both are correct as written and are left alone. Also fixes the same defect's other half in the same five lines: the declared return type omitted `publish`, which the backend adds as `"running"` when it republishes. Correcting the prose while leaving the type silent about the same conditional would be the half-applied fix this sequence has twice been caught making. Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0, ruff (the CI command, from `services/api`) 0, `test_file_sizes.py` 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…module but not the codebase Moves `elementDetailing`, `classify`, `applyDetailingRules`, `validateDetailing` and `attachDocument` out of `client.ts` into a new `api/detailing.ts`. `client.ts` goes 687 -> 665, 65 methods above the STAYING banner. WHAT THEY ANSWER: what informational carriers are attached to this element, write them, and which are missing? THE WITNESS IS A 1:1 AND TOTAL FIELD-TO-WRITER MAP, the shape (96) established. `element_detailing` walks `HasAssociations` and branches on exactly two relationship types, and `detailing.py` holds exactly two writers, one per response array: classifications[] <- classify (IfcRelAssociatesClassification) documents[] <- attachDocument (IfcRelAssociatesDocument) The map is read out of the reader's own body, not matched on names. The other two methods are those same writes automated and audited: `applyDetailingRules` runs the condition-to-content rule set and writes both carrier kinds, `validateDetailing` reports elements a rule applies to that lack the code. TOTAL OVER THE MODULE, NOT THE CODEBASE, and the difference is the claim. `attachOmDocument` — moved to `model.ts` in (96) — wraps the SAME `detailing.attach_document` and also lands in `documents[]`, so "these are all the writers of this reader's fields" is false. That overlap was recorded when it moved, which is why it was on hand to qualify this slice rather than being found in review. ADJACENCY AGREED WITH THE ANSWER AND IS NOT EVIDENCE FOR IT. These five were contiguous in `client.ts`; unlike (95), where non-contiguity was the whole argument, a positional split would have found this set too. Stated precisely because it looks like support. ALSO RECORDED: `api.classify()` has no call site — `detailingSection.ts` drives the recipe through the generic `authorAndReload` path, bypassing the typed method, and `clientCallers.test.ts` counts it reached because it matches bare string literals as well as calls. That looseness is deliberate per that file's own docstring, so it is a limit of the gate rather than a defect in it. The new mixin needs `editIfc`, so it declares `NeedsEditIfc` and composes outside `withAuthoring`; `compositionOrder.test.ts` gains a fourth assertion. MUTATION-CHECKED: relaxing the constraint to `Ctor<any>` produces "TS2578: Unused '@ts-expect-error' directive" on exactly the new line, so it fails for the reason claimed rather than merely passing. The extraction removed 27 lines and the banner plus import added 5 back — caught by the ratchet, not by me, the same way (94) found its own banner growth. Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0, ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`, `test_claude_md_gates.py`, `test_roadmap_status.py` all 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #414 was squash-merged, so origin/main carries (97) as 3faae36 while this branch still held the pre-squash commits. The content diff between the remote branch and origin/main was empty before this merge, so the branch held no unmerged work; -s ours keeps this branch's tree (origin/main plus (98)) and reconciles the histories without rewriting the remote branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…s wrong until now
Moves `contentCatalog`, `placeContent` and `importContent` out of `client.ts`
into `api/authoring.ts`. `client.ts` goes 665 -> 648, 62 methods above the
STAYING banner.
WHAT THEY ANSWER: what pre-made content can I place, and place it?
THE WITNESS IS A ROLE-FOR-ROLE PARALLEL with the family shelf already in that
file, read off the signatures rather than the shared noun:
catalog reader familyCatalog() {count, categories: Record<..>}
contentCatalog() {count, note, groups: Record<..>}
placer placeFamily(pid, family, position)
placeContent(pid, category, point, name)
multipart import async importFamilies(pid, file, ..)
async importContent(pid, file, opts)
Three roles, three methods each, matching shapes and arities. A parallel between
two method TRIPLES is structural; "both are shelves" would have been a shared
word, which is the grouping (88) and (89) each had to reject.
THE DESTINATION'S OWN FIRST LINE WAS WRONG UNTIL THIS COMMIT. `authoring.ts` has
described itself as holding "the family/content shelf" while containing ZERO
content methods — the word's only other appearances there are an HTTP header and
a sentence about IFC *type* content. The docstring stated an intended scope as
fact.
That is recorded as corroboration that was FALSE, not as evidence. A header
agreeing with the answer is worth nothing until someone checks whether it is
true, and this one had been wrong for as long as it had existed. It is the
smallest possible instance of the drift the project instructions keep warning
about.
Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py` all 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #431 was squash-merged, so origin/main carries (98) as ae4bc0e while this branch still held the pre-squash commits. The content diff between the remote branch and origin/main was empty before this merge, so the branch held no unmerged work; -s ours keeps this branch's tree (origin/main plus (99)) and reconciles the histories without rewriting the remote branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…ed at its real strength
Moves `elementConnections` and `connectElements` out of `client.ts` into
`api/model.ts`. `client.ts` goes 648 -> 642, 60 methods above the STAYING
banner.
WHAT THEY ANSWER: what is physically joined to what, and record a joint?
THE PAIR IS BOUND BY THE BACKEND NAMING ITS OWN WRITER, the (96) shape: the
`/element-connections` route docstring reads "Author edges with the
`connect_elements` recipe (POST /edit with {guid_a, guid_b})". Reader and
writer, one relationship type (IfcRelConnectsElements), both marked B5.
THE DESTINATION ARGUMENT IS WEAKER THAN THE PAIRING ARGUMENT AND IS LABELLED SO.
`model.ts` owns `modelGraphStats`, whose `by_rel` counts the IFC relationship
graph BY RELATION — IfcRelConnectsElements being one — and `graphNeighbors`,
which walks it. So this pair is one relation of a graph the file already reads,
plus its authoring verb. That is a SPECIALISATION, not an identity: the graph
methods are generic traversal over every IfcRel*, these two are one relation
with a verb attached. The pairing is evidenced; the placement is a judgement,
and collapsing the two into one confident sentence is the overstatement this
sequence keeps catching.
TWO CANDIDATES REJECTED ON CHECKABLE GROUNDS. `connections.ts` is the trap: it
is DATA-SOURCE connections — SQL, ACC, Procore — sharing nothing with this but
the English word, and it is the file a name-based search lands on first. (97)
found two version stacks behind one word; this is the same collision in a
destination rather than a source. `elements.ts` holds element ATTRIBUTES and
views, and a relationship between two elements is not an attribute of either.
`addBasePlate`/`addShearTab` did not come despite sharing `connections.py` with
these: a backend module is a HOW, the grouping (89) had to reject, and those two
author PHYSICAL assemblies rather than relationship edges.
FOUND WHILE DERIVING, RECORDED NOT FIXED: `add_connection_assembly` (B5,
IfcRelConnectsWithRealizingElements) has no client method anywhere in
`apps/web/src` — a backend recipe with no web exposure, the class (93) recorded
for three MEP recipes.
Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py` all 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #432 was squash-merged, so origin/main carries (99) as a58f047 while this branch still held the pre-squash commits. The content diff between the remote branch and origin/main was empty before this merge, so the branch held no unmerged work; -s ours keeps this branch's tree (origin/main plus (100)) and reconciles the histories without rewriting the remote branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThis change moves ChangesElement connection API
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to Element-connection reading and writing remain available through the web API after relocation to the model domain, with the public surface check updated. No merge-blocking production risk is identified. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review Generated by Claude Code |
|
✅ Action performedReview finished.
|
|
Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here. So far, Strix has reviewed 28 pull requests, surfaced 3 security issues (1 critical/high) and blocked 2 risky merges across this workspace. |
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
What & why
apps/web/src/api/client.tsgoes 648 → 642 (60 methods above the STAYING banner, 4 below).elementConnectionsandconnectElementsmove to the existingapps/web/src/api/model.ts.What they answer: what is physically joined to what, and record a joint? Continues SCALE-SEAM in
docs/roadmap.md.The backend names its own writer
The
/element-connectionsroute docstring reads: "Author edges with theconnect_elementsrecipe(
POST /editwith{guid_a, guid_b})." Reader and writer, one relationship type(
IfcRelConnectsElements), both marked B5. That is the (96) shape, and it is what carries the pair.The destination argument is weaker, and is labelled as such
model.tsownsmodelGraphStats— whoseby_relcounts the IFC relationship graph by relation,and
IfcRelConnectsElementsis one of those relations — plusgraphNeighbors, which walks it. Sothis pair is one relation of a graph the file already reads, plus its authoring verb.
That is a specialisation, not an identity. The graph methods are generic traversal over every
IfcRel*; these two are one relation with a verb attached. The pairing is evidenced; the placementis a judgement. I've stated them separately rather than merging them into one confident sentence,
because that merge is the overstatement this sequence keeps catching.
Two candidates rejected on checkable grounds
connections.tsis the trap. It is data-source connections — SQL, ACC, Procore — sharingnothing with this but the English word, and it is the file a name-based search lands on first. (97)
found two version stacks behind one word; this is the same collision in a destination rather than
a source.
elements.tsholds element attributes and views (properties, 5D, colouring, QA, costs). Arelationship between two elements is not an attribute of either.
addBasePlateandaddShearTabdid not come despite sharingservices/data/src/aec_data/connections.pywith these: a backend module is a HOW — the grouping (89)had to reject — and those two author physical assemblies (plates, bolts) rather than
IfcRelConnectsElementsedges.Found while deriving, recorded not fixed
add_connection_assembly(B5,IfcRelConnectsWithRealizingElements) has no client methodanywhere in
apps/web/src— a backend recipe with no web exposure, the same class (93) recordedfor three MEP recipes.
Note on this branch's history
#432 was squash-merged, so
origin/maincarries (99) asa58f0471while this branch still held thepre-squash commits. The content diff against
origin/mainwas verified empty first, so the branchheld no unmerged work; the histories were reconciled with a
-s oursmerge rather than a force-push.Checklist (mirrors CONTRIBUTING.md)
cd services/api && python -m ruff check ../..cleantest_file_sizes.py,test_claude_md_gates.py,test_roadmap_status.pyexit 0;full suite running as a cross-check
npx tsc --noEmit0,npm run lint0,npm run build0;npx vitest run src/api27 files / 119 testsCHANGELOG.mdentry added (newest at top);docs/roadmap.mdupdated🤖 Generated with Claude Code
https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
Generated by Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests