Skip to content
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,62 @@ Reader and unwired writer on one panel — still the reason not to separate them

Pin 731 → 727. **80 above the banner, still no map.**

Thirty-seventh follow-on on the same version: **SCALE-SEAM (98)** — *detailing carriers, and a
field map that is total over one module but not over the codebase*.

Five methods out of `client.ts` (`687 → 665`) into a new `apps/web/src/api/detailing.ts`:
`elementDetailing`, `classify`, `applyDetailingRules`, `validateDetailing`, `attachDocument`.
**What they answer: what informational carriers are attached to this element, write them, and which
are missing.**

### A 1:1 and total field-to-writer map

The reader's response has exactly two carrier arrays, and `services/data/src/aec_data/detailing.py`
holds exactly two writers, one per array:

| response field | writer | IFC relationship |
|---|---|---|
| `classifications[]` | `classify` | `IfcRelAssociatesClassification` |
| `documents[]` | `attachDocument` | `IfcRelAssociatesDocument` |

`element_detailing` walks `HasAssociations` and branches on precisely those two relationship types —
nothing else contributes a field — so the map comes from the reader's own body rather than from
matching names. The other two methods are those same two writes under automation:
`applyDetailingRules` runs the condition-to-content rule set and writes both carrier kinds, and
`validateDetailing` reports elements a rule applies to that lack the required code.

### Total over the module, not over the codebase

`attachOmDocument` — moved to `model.ts` in (96) — is a purpose-tagged wrapper of the **same**
`detailing.attach_document`, so it also writes `IfcRelAssociatesDocument` and its output lands in
`documents[]`. So *"these are all the writers of this reader's fields"* is **false**. The claim the
evidence supports is narrower and is the one made: the map is 1:1 and total **over `detailing.py`**.
*That overlap was named when `attachOmDocument` moved, which is why it was available to qualify this
slice instead of being discovered by a reviewer.*

### Adjacency agreed with the answer, and is not evidence for it

These five were **contiguous** in `client.ts` (119–145). Unlike (95), where non-contiguity was the
whole argument for grouping by what methods answer, a positional split would have found this set
too. That is worth stating precisely *because* it looks like support: a grouping that happens to
coincide with adjacency is not thereby better evidenced.

Also recorded: **`api.classify()` has no call site.** `viewer/tools/detailingSection.ts` drives the
recipe through the generic `authorAndReload("classify", …)` path, bypassing the typed method.
`api/clientCallers.test.ts` counts it as reached because it matches bare string literals as well as
calls — a looseness that file's own docstring declares deliberate, preferring a higher ceiling to a
false unreachability report. Noted so the next reader of `detailing.ts` does not assume the method
is live.

The new mixin needs `editIfc`, so it declares `NeedsEditIfc` and composes outside `withAuthoring`;
`api/compositionOrder.test.ts` gains a fourth line asserting that. **That assertion was
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.

`client.ts` is 65 methods above the STAYING banner and 4 below. *The extraction removed 27 lines and
the banner plus import added 5 back — caught by the ratchet, not by me, which is the same way (94)
found its own banner growth.*

Thirty-sixth follow-on on the same version: **SCALE-SEAM (97)** — *the undo stack, and a
destination that looked right and was not*.

Expand Down
36 changes: 7 additions & 29 deletions apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { withOperations } from "./operations";
import { withClientPortal } from "./clientPortal";
import { withCreDeal } from "./creDeal";
import { withAnnotate } from "./annotate";
import { withDetailing } from "./detailing";
import { withResilience } from "./resilience";
import { withResponsibility } from "./responsibility";
import { withCodeCheck } from "./codecheck"; import { withDealMemory } from "./dealMemory";
Expand Down Expand Up @@ -69,7 +70,7 @@ import type {

// Transport (baseUrl, token, json/_pdfPost/url/health) lives in HttpCore; ApiClient adds the typed
// domain methods below. Every `api.method()` call site is unchanged by the split.
export class ApiClient extends withAnnotate(withCreDeal(withClientPortal(withResilience(withResponsibility(withOperations(withAccounting(withDealMemory(withPdfTools(withCodeCheck(withSpecialty(withIds(withEvm(withRisk(withEntitlements(withPrecon(withAi(withTopics(withMep(withDocuments(withModels(withElements(withDrawingSheets(withDrawingSet(withMarkup(withSync(withConnections(withDocQa(withFinance(withContracts(withAuth(withProforma(withDesignOptions(withRoutines(withCost(withProcurement(withEstimate(withModules(withModel(withSchedule(withLibrary(withAssetRights(withAuthoring(HttpCore))))))))))))))))))))))))))))))))))))))))))) {
export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClientPortal(withResilience(withResponsibility(withOperations(withAccounting(withDealMemory(withPdfTools(withCodeCheck(withSpecialty(withIds(withEvm(withRisk(withEntitlements(withPrecon(withAi(withTopics(withMep(withDocuments(withModels(withElements(withDrawingSheets(withDrawingSet(withMarkup(withSync(withConnections(withDocQa(withFinance(withContracts(withAuth(withProforma(withDesignOptions(withRoutines(withCost(withProcurement(withEstimate(withModules(withModel(withSchedule(withLibrary(withAssetRights(withAuthoring(HttpCore)))))))))))))))))))))))))))))))))))))))))))) {
/**
* R22-PHOTO-CV — attach a field photo to an element and get the server's read on it back.
*
Expand Down Expand Up @@ -116,33 +117,6 @@ export class ApiClient extends withAnnotate(withCreDeal(withClientPortal(withRes
return this.json<{ by: string; buckets: Record<string, string[]>; counts: Record<string, number>; element_count: number }>(
`/projects/${pid}/5d/heatmap?by=${by}`);
}
/** W11 Track D: one element's attached carriers — classification codes + documents (details/instructions). */
elementDetailing(pid: string, guid: string) {
return this.json<{ guid: string; name: string; ifc_class: string;
classifications: { system: string | null; code: string | null; title: string | null }[];
documents: { identification: string | null; name: string | null; location: string | null; description: string | null }[] }>(
`/projects/${pid}/detailing/${encodeURIComponent(guid)}`);
}
/** W11 Track D: classify elements with a keynote/spec/element code (UniFormat/MasterFormat/OmniClass). */
classify(pid: string, guids: string[], system: string, code: string, name?: string, edition?: string, publish = true) {
return this.editIfc(pid, "classify", { guids, system, code, name, edition }, publish);
}
/** W11 D3: auto-detail — run the condition→content rule set (e.g. exterior window → IBC flashing
* detail + 08 51 00), writing code/detail bundles to every matching element. */
applyDetailingRules(pid: string, publish = true) {
return this.editIfc(pid, "apply_detailing_rules", {}, publish);
}
/** W11 D3: IDS-style QA — elements that a rule applies to but are missing their required keynote/spec code. */
validateDetailing(pid: string) {
return this.json<{ rules_evaluated: number; gaps: number;
elements: { rule: string; guid: string; name: string; missing: string }[] }>(
`/projects/${pid}/detailing/rules/validate`);
}
/** W11 Track D: attach a document (detail drawing / installation instruction) to elements. */
attachDocument(pid: string, guids: string[], name: string,
opts: { location?: string; identification?: string; description?: string; purpose?: string } = {}, publish = true) {
return this.editIfc(pid, "attach_document", { guids, name, ...opts }, publish);
}
/** W11 B6: author a base plate + anchor bolts under a steel column (fabrication assembly). */
addBasePlate(pid: string, columnGuid: string, opts: { bolts?: number; width?: number; depth?: number } = {}, publish = true) {
return this.editIfc(pid, "add_base_plate", { column_guid: columnGuid, ...opts }, publish);
Expand Down Expand Up @@ -592,7 +566,7 @@ export class ApiClient extends withAnnotate(withCreDeal(withClientPortal(withRes
// through (87) worked through, and they are recorded here as DECIDED rather than pending.
//
// **THIS IS NOT THE END OF SCALE-SEAM, and a previous version of this banner implied it was.**
// 70 methods still sit ABOVE this line — `disciplineTree`, `classify`, `specManual`, `editUndo`,
// 65 methods still sit ABOVE this line — `disciplineTree`, `classify`, `specManual`, `editUndo`,
// `energyModel`, `propmapPlan`, `camReconciliation` and the rest. They were never inside the
// CX-1 banner, so no map has ever covered them. The UNFILED map described the TAIL of this file,
// not the file.
Expand Down Expand Up @@ -659,6 +633,10 @@ export class ApiClient extends withAnnotate(withCreDeal(withClientPortal(withRes
// 70. `editIfc`, already there, is the PUSH they pop. `model.ts` looked right and is not: its
// `modelVersions` reads a DIFFERENT stack. Reasoning in `authoring.ts`'s header.
//
// (98) took DETAILING CARRIERS to a new `detailing.ts`, leaving 65. The reader's two carrier
// arrays map 1:1 onto `detailing.py`'s two writers, plus the rule engine that writes both and
// the audit that reports gaps. Reasoning in `detailing.ts`'s header.
//
// the four that stay enumOptions, searchAll, attachmentUrl, templates
enumOptions(pid: string) {
return this.json<Record<string, Record<string, string[]>>>(`/projects/${pid}/enum-options`);
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/api/compositionOrder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { HttpCore } from "./httpCore";
import { withAnnotate } from "./annotate";
import { withMep } from "./mep";
import { withModel } from "./model";
import { withDetailing } from "./detailing";

describe("mixins requiring editIfc", () => {
it("reject a base that lacks it, so a bad chain order fails at compile time", () => {
Expand All @@ -41,6 +42,8 @@ describe("mixins requiring editIfc", () => {
void (() => withMep(HttpCore));
// @ts-expect-error withModel needs NeedsEditIfc; bare HttpCore has no editIfc.
void (() => withModel(HttpCore));
// @ts-expect-error withDetailing needs NeedsEditIfc; bare HttpCore has no editIfc.
void (() => withDetailing(HttpCore));
expect(true).toBe(true);
});
});
78 changes: 78 additions & 0 deletions apps/web/src/api/detailing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/** Detailing carriers: the informational attachments an element carries — classification codes
* (keynote / spec / element) and associated documents (details, installation instructions) — the
* rule engine that writes them in bulk, and the QA that audits what is missing.
*
* **SCALE-SEAM (98).** *What informational carriers are attached to this element, write them, and
* which are missing?* Five methods: `elementDetailing` (the inspector), `classify` and
* `attachDocument` (the two writers), `applyDetailingRules` (the rule engine that writes both in
* bulk) and `validateDetailing` (the gap audit).
*
* **The witness is a 1:1 AND TOTAL field-to-writer map**, the shape (96) established. The reader's
* response has exactly two carrier arrays, and `services/data/src/aec_data/detailing.py` holds
* exactly two writers, one per array:
*
* `classifications[]` <- `classify` (writes `IfcRelAssociatesClassification`)
* `documents[]` <- `attachDocument` (writes `IfcRelAssociatesDocument`)
*
* `element_detailing` walks `HasAssociations` and branches on precisely those two relationship
* types — nothing else contributes a field — so the map is derived from the reader's own body, not
* inferred from names. The other two methods are the same two writes under automation:
* `applyDetailingRules` runs the condition-to-content rule set and writes BOTH carrier kinds, and
* `validateDetailing` reports elements a rule applies to that lack the required code.
*
* **What this does NOT claim, and it is the same limit (96) recorded.** `attachOmDocument` —
* moved to `model.ts` in (96) — is a purpose-tagged wrapper of the SAME
* `detailing.attach_document`, so it also writes `IfcRelAssociatesDocument` and its output appears
* in `documents[]`. *"These are all the writers of this reader's fields"* is therefore **false**.
* The map is 1:1 and total over `detailing.py`, which is the claim the evidence supports; one
* method answering the turnover question reaches the same carrier from another file, and that
* overlap was named when it moved rather than discovered here.
*
* *Two weaker witnesses, labelled as such rather than promoted.* `viewer/tools/detailingSection.ts`
* calls only the two READERS, so it corroborates them and bounds nothing; and these five were
* CONTIGUOUS in `client.ts` (119-145), which means — unlike (95), where non-contiguity was the
* whole argument — a positional split would have found this set too. **Adjacency agreeing with the
* answer is not evidence for it**; it is worth stating precisely because it looks like support.
*
* *`api.classify()` has no call site.* `viewer/tools/detailingSection.ts` drives the recipe through
* the generic `authorAndReload("classify", ...)` path, so the typed method is bypassed.
* `api/clientCallers.test.ts` counts it as reached because it matches bare string literals too — a
* looseness that file's own docstring declares deliberate, preferring a higher ceiling to a false
* unreachability report. Recorded here because the next reader of this file will otherwise assume
* the method is live.
*/
import type { NeedsEditIfc } from "./types";

type Ctor<T> = new (...args: any[]) => T;

export function withDetailing<TBase extends Ctor<NeedsEditIfc>>(Base: TBase) {
return class Detailing extends Base {
/** W11 Track D: one element's attached carriers — classification codes + documents (details/instructions). */
elementDetailing(pid: string, guid: string) {
return this.json<{ guid: string; name: string; ifc_class: string;
classifications: { system: string | null; code: string | null; title: string | null }[];
documents: { identification: string | null; name: string | null; location: string | null; description: string | null }[] }>(
`/projects/${pid}/detailing/${encodeURIComponent(guid)}`);
}
/** W11 Track D: classify elements with a keynote/spec/element code (UniFormat/MasterFormat/OmniClass). */
classify(pid: string, guids: string[], system: string, code: string, name?: string, edition?: string, publish = true) {
return this.editIfc(pid, "classify", { guids, system, code, name, edition }, publish);
}
/** W11 D3: auto-detail — run the condition→content rule set (e.g. exterior window → IBC flashing
* detail + 08 51 00), writing code/detail bundles to every matching element. */
applyDetailingRules(pid: string, publish = true) {
return this.editIfc(pid, "apply_detailing_rules", {}, publish);
}
/** W11 D3: IDS-style QA — elements that a rule applies to but are missing their required keynote/spec code. */
validateDetailing(pid: string) {
return this.json<{ rules_evaluated: number; gaps: number;
elements: { rule: string; guid: string; name: string; missing: string }[] }>(
`/projects/${pid}/detailing/rules/validate`);
}
/** W11 Track D: attach a document (detail drawing / installation instruction) to elements. */
attachDocument(pid: string, guids: string[], name: string,
opts: { location?: string; identification?: string; description?: string; purpose?: string } = {}, publish = true) {
return this.editIfc(pid, "attach_document", { guids, name, ...opts }, publish);
}
};
}
4 changes: 4 additions & 0 deletions apps/web/src/api/surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ describe("the API client's public surface", () => {
// (97) the undo stack -> authoring.ts. All three have LIVE call sites in app.ts's S4 block,
// so a drop here breaks the undo/redo buttons, not just the surface count.
"editHistory", "editUndo", "editRedo",
// (98) detailing carriers -> detailing.ts. `elementDetailing` and `validateDetailing` have
// live call sites; `classify` is driven through the generic recipe path, so the SURFACE check
// is the only thing that would notice it vanishing.
"elementDetailing", "classify", "applyDetailingRules", "validateDetailing", "attachDocument",
]) {
expect(surface.has(k), `${k}() vanished — a call site is now broken`).toBe(true);
}
Expand Down
Loading