Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,41 @@ 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-eighth follow-on on the same version: **SCALE-SEAM (99)** — *the content half of a shelf the
destination's own first line already claimed*.

Three methods out of `client.ts` (`665 → 648`) into the existing `apps/web/src/api/authoring.ts`:
`contentCatalog`, `placeContent`, `importContent`. No new mixin. **What they answer: what pre-made
content can I place, and place it.**

### A role-for-role parallel, read off the signatures

The family shelf was already in `authoring.ts`. The content shelf is the same three roles, with
matching shapes and arities:

| role | family | content |
|---|---|---|
| catalog reader | `familyCatalog()` → `{count, categories: Record<…>}` | `contentCatalog()` → `{count, note, groups: Record<…>}` |
| placer | `placeFamily(pid, family, position)` | `placeContent(pid, category, point, name)` |
| multipart importer | `async importFamilies(pid, file, …)` | `async importContent(pid, file, opts)` |

**A parallel between two method *triples* is structural.** "Both are shelves" would have been a
shared word — the grouping (88) and (89) each had to reject — so the argument is deliberately the
signatures, not the noun.

### The destination's header was wrong until this commit

`authoring.ts` line 1 has read *"the family/content shelf"* while the file held **zero** content
methods; the only other occurrences of the word were an HTTP header and a sentence about IFC *type*
content. The docstring described an intended scope as though it were a fact.

That is the same class as every drift the project instructions warn about, at the smallest possible
scale: **prose asserting an arrangement that nothing checked.** It is recorded as corroboration that
was *false* rather than as evidence — the parallel above is what carries the slice, and a header
that agreed with me would have been worth nothing if I had not checked whether it was true.

`client.ts` is 62 methods above the STAYING banner and 4 below.

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*.

Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/api/authoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@
* SCALE-SEAM ⓼ adds groups and assemblies — *how are these elements grouped?*
* List, inspector, create group/assembly, parametric array. Detailing stayed.
*
* **SCALE-SEAM (99) adds the CONTENT half of the shelf this file's own first line already claimed.**
* `contentCatalog`, `placeContent`, `importContent` — *what pre-made content can I place, and place
* it.* The witness is a ROLE-FOR-ROLE PARALLEL with the family shelf already here, derived from the
* signatures rather than from the shared word "shelf":
*
* 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 matching arities. *A parallel between two
* method TRIPLES is structural; "both are shelves" would have been a word, which is the grouping
* (88) and (89) each had to reject.*
*
* **The header above was wrong until this commit, and that is the small finding.** Line 1 has said
* "the family/content shelf" while this file held **zero** content methods — the only other
* occurrences of the word were an HTTP header and a sentence about IFC *type* content. So the
* docstring described an intended scope as though it were a fact. *That is the same class as every
* drift the project instructions warn about, at the smallest possible scale: prose asserting an
* arrangement nothing checked.* It is corroboration that was FALSE, not evidence — the parallel
* above is what carries the slice.
*
* **SCALE-SEAM (97) adds the UNDO STACK — *what has been done to this model, and can I take it
* back?*** `editHistory` (can_undo/can_redo + depths), `editUndo` and `editRedo`. They belong in
* THIS file because `editIfc` — already here — is the PUSH they pop: `authoring.py` records the
Expand Down Expand Up @@ -382,6 +403,27 @@ export function withAuthoring<TBase extends Ctor<HttpCore>>(Base: TBase) {
publish?: string }>(
`/projects/${pid}/edit/redo`, { method: "POST", body: JSON.stringify({ publish }) });
}
/** CONTENT-1: the curated content catalog (logistics / furniture / landscaping → IFC class + phase). */
contentCatalog() {
return this.json<{ count: number; note: string; groups: Record<string, { key: string; ifc_class: string;
phase: string | null; classification: string; default_dims_m: number[] }[]> }>(`/content/catalog`);
}
/** CONTENT-1: place a catalogued content item at an [E,N] point (optionally with a supplied mesh). */
placeContent(pid: string, category: string, point: [number, number], name?: string, publish = true) {
return this.editIfc(pid, "place_content", { category, point, ...(name ? { name } : {}) }, publish);
}
/** CONTENT-1 (import): upload a detailed mesh (glTF/GLB/OBJ/STL/PLY) → auto-classified + placed as the
* right IFC via place_content. Category auto-detected from the filename unless given. */
async importContent(pid: string, file: File, opts: { category?: string; e?: number; n?: number;
scale?: number; name?: string; storey?: string } = {}) {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(opts)) if (v !== undefined && v !== "") q.set(k, String(v));
const fd = new FormData(); fd.append("file", file);
const r = await fetch(this.url(`/projects/${pid}/content/import?${q.toString()}`),
{ method: "POST", body: fd, headers: this.authHeaders() });
if (!r.ok) throw new Error((await r.text()) || `HTTP ${r.status}`);
return r.json() as Promise<{ guid: string; ifc_class: string; category: string; faces: number; publish?: string }>;
}
};
}

Expand Down
27 changes: 5 additions & 22 deletions apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,27 +152,6 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
phasing: Record<string, number>; lod: Record<string, number>;
hygiene: { issues: number | null; clean: boolean | null } }>(`/projects/${pid}/scene-digest`);
}
/** CONTENT-1: the curated content catalog (logistics / furniture / landscaping → IFC class + phase). */
contentCatalog() {
return this.json<{ count: number; note: string; groups: Record<string, { key: string; ifc_class: string;
phase: string | null; classification: string; default_dims_m: number[] }[]> }>(`/content/catalog`);
}
/** CONTENT-1: place a catalogued content item at an [E,N] point (optionally with a supplied mesh). */
placeContent(pid: string, category: string, point: [number, number], name?: string, publish = true) {
return this.editIfc(pid, "place_content", { category, point, ...(name ? { name } : {}) }, publish);
}
/** CONTENT-1 (import): upload a detailed mesh (glTF/GLB/OBJ/STL/PLY) → auto-classified + placed as the
* right IFC via place_content. Category auto-detected from the filename unless given. */
async importContent(pid: string, file: File, opts: { category?: string; e?: number; n?: number;
scale?: number; name?: string; storey?: string } = {}) {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(opts)) if (v !== undefined && v !== "") q.set(k, String(v));
const fd = new FormData(); fd.append("file", file);
const r = await fetch(this.url(`/projects/${pid}/content/import?${q.toString()}`),
{ method: "POST", body: fd, headers: this.authHeaders() });
if (!r.ok) throw new Error((await r.text()) || `HTTP ${r.status}`);
return r.json() as Promise<{ guid: string; ifc_class: string; category: string; faces: number; publish?: string }>;
}
/** W11 E8: validate an edit's params against the authoring guardrails without applying it. */
editPrecheck(pid: string, recipe: string, params: Record<string, unknown>) {
return this.json<{ ok: boolean; errors: string[]; warnings: string[] }>(
Expand Down Expand Up @@ -566,7 +545,7 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
// 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.**
// 65 methods still sit ABOVE this line — `disciplineTree`, `classify`, `specManual`, `editUndo`,
// 62 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 @@ -637,6 +616,10 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
// 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.
//
// (99) took the CONTENT SHELF — `contentCatalog`/`placeContent`/`importContent` — to
// `authoring.ts`, leaving 62. Role-for-role parallel with the family shelf already there; that
// file's first line had claimed "family/content shelf" while holding no content method.
//
// 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
2 changes: 2 additions & 0 deletions apps/web/src/api/surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ describe("the API client's public surface", () => {
// 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",
// (99) the content shelf -> authoring.ts, joining the family triple it parallels.
"contentCatalog", "placeContent", "importContent",
]) {
expect(surface.has(k), `${k}() vanished — a call site is now broken`).toBe(true);
}
Expand Down
19 changes: 18 additions & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -3135,7 +3135,7 @@ verbs, with a command bar as the escape hatch to everything); and **role-shaped
banner is renamed UNFILED → STAYING and now says exactly that; the next slices work the 126,
and there is no map for them yet.

**(88)–(98) took sixty-one of those 126 — 65 remain, and there is STILL no map.** A new
**(88)–(99) took sixty-four of those 126 — 62 remain, and there is STILL no map.** A new
`apps/web/src/api/operations.ts` holds the operate-phase cluster: *the building is built and
running.* Maintenance (`cmmsGeneratePm`, `cmmsKpis`), consumption (`energyActual`,
`energyBenchmarkStatus`, `esgSummary`), condition and capital (`fcaIndex`, `fcaPortfolio`,
Expand Down Expand Up @@ -3373,6 +3373,23 @@ verbs, with a command bar as the escape hatch to everything); and **role-shaped
relaxing the constraint to `Ctor<any>` produces `TS2578` on exactly the new line, so it fails for
the reason claimed.

**(99) took the CONTENT SHELF to `apps/web/src/api/authoring.ts`** — `contentCatalog`,
`placeContent`, `importContent`, answering *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: `familyCatalog`/`contentCatalog` are both catalog readers
returning `{count, …Record<…>}`, `placeFamily`/`placeContent` are both placers, and
`importFamilies`/`importContent` are both async multipart importers. Three roles, three methods
each. **A parallel between two method TRIPLES is structural**; *"both are shelves"* would have been
a word, which is the grouping (88) and (89) each had to reject.

**And 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. *Recorded as corroboration that was FALSE rather than
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 these instructions keep warning about.

**(93) finished SCALE-SEAM ⑲.** Two methods to the existing `apps/web/src/api/mep.ts` —
`connectMep` and `addMepFitting` — which that file has claimed by name since ⑲ under the note
*"call `editIfc` (`/edit`) and stay"*. **That note recorded the symptom without diagnosing the
Expand Down
2 changes: 1 addition & 1 deletion services/api/test_file_sizes.py

Large diffs are not rendered by default.