Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
44a6ed4
SCALE-SEAM (95) — element state: two read/write pairs, and why the ma…
claude Sep 4, 2026
121c095
SCALE-SEAM (96) — the as-built question's aggregate reader, and a wit…
claude Sep 4, 2026
2f09a5c
Merge already-squashed (95) branch history
claude Sep 4, 2026
0472f97
SCALE-SEAM (97) — the undo stack, and a destination that looked right…
claude Sep 4, 2026
66ab67a
Merge already-squashed (96) branch history
claude Sep 4, 2026
58525e1
Correct the undo/redo republish docs — all three sites, not just the …
claude Sep 4, 2026
75f8597
SCALE-SEAM (98) — detailing carriers, and a field map total over one …
claude Sep 4, 2026
f2926a5
Merge already-squashed (97) branch history
claude Sep 4, 2026
83e149e
SCALE-SEAM (99) — the content shelf, and a destination header that wa…
claude Sep 4, 2026
17103db
Merge already-squashed (98) branch history
claude Sep 4, 2026
2a8f46f
SCALE-SEAM (100) — the element-connection pair, and a destination nam…
claude Sep 4, 2026
6d68219
Merge already-squashed (99) branch history
claude Sep 4, 2026
370a1f8
Merge already-squashed (95)-(100) branch history
claude Sep 4, 2026
f5d6751
R22-ENTITLEMENT ⑤ — an agency review comment becomes an RFI somebody …
claude Sep 4, 2026
5c79d56
Merge branch history — reconcile after the (95)-(100) squashes
claude Sep 4, 2026
d826115
R22-ENTITLEMENT ⑤ review: promotion claims the comment atomically, an…
claude Sep 4, 2026
9f048eb
Merge branch history after the #434 squash — content already identical
claude Sep 4, 2026
c437a7f
R24-REPORTS-BY-MOMENT — a finished pack can be sent, not only downloaded
claude Sep 4, 2026
87cc0e8
Review round on #435: five findings, all verified real, all fixed
claude Sep 4, 2026
67dfc36
Second review round on #435: the port fix is the root cause my first …
claude Sep 4, 2026
eff9568
Merge after the #435 squash
claude Sep 4, 2026
0fadd1c
Only committed capital owns anything — cap table and waterfall
claude Sep 4, 2026
87f3a59
Merge after the #436 squash
claude Sep 4, 2026
2fa0692
SCALE-SEAM (101) — design-phase predicted performance, client.ts 642 …
claude Sep 4, 2026
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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ All notable changes to Massing. Releases are signed, auto-updating desktop build
(Windows / macOS / Linux); the updater always serves the latest. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/).

## Unreleased — SCALE-SEAM (101): design-phase predicted performance

Six methods out of `client.ts` (**642 → 603**). Five to a new
`apps/web/src/api/designPerformance.ts` — `energy`, `energyModel`, `energyExportUrl`,
`carbonComplianceReport`, `projectCarbon` — and `benchmarkCosts` to `cost.ts`.

**The seam was drawn by earlier slices, not this one, which is the whole witness.**
`operations.ts`'s own header records why the carbon half did not go there: *"`projectCarbon` is
EMBODIED carbon — a design-phase estimate. The GHG figures in `esgSummary` come from metered utility
data. Same molecule, opposite ends of the asset life."* `models.ts` records the parallel call for
`/energy`, and `operations.ts` does hold `/energy/actual`. Prediction versus measurement, committed
to twice independently — these five are the prediction side.

Deliberately **not** named `environmental.ts`: that names the topic both halves share, and would
re-blur the seam `operations.ts` drew.

**A planned `benchmarks.ts` was abandoned on evidence.** `cost.ts` already held `unitRates`
(`/benchmarks/unit-rates`) and `schedule.ts` holds `benchmarksPullPlanning`
(`/benchmarks/pull-planning`), so the repo already distributes that prefix by what each method
answers; grouping the remaining three by route would have contradicted two live placements.
`benchmarkResponseRates` and `spaceUtilBenchmarks` stayed — no mixin owns their question, and
inventing a home on a guess is what produced this file's UNFILED banner.

The DOC-STRAND gate caught the extraction stranding `unitRates`' doc comment above the inserted
block; reunited rather than deleted.

## Unreleased — only committed capital owns anything (cap table + waterfall)

`capital.cap_table` summed `commitment` across every investor whatever their workflow state. A
Expand Down
45 changes: 3 additions & 42 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 { withDesignPerformance } from "./designPerformance";
import { withDetailing } from "./detailing";
import { withResilience } from "./resilience";
import { withResponsibility } from "./responsibility";
Expand Down Expand Up @@ -61,7 +62,7 @@ export * from "./library";
export type { ClashResult } from "./clash";
import type {
Dashboard,
DisciplineTree, EnergyResult, ModulePin, RoomAllocation,
DisciplineTree, ModulePin, RoomAllocation,
PropMapRule,
SpecManual, WorkItem, VitalsPayload,
DiligenceReadiness, MasterBuilderBrief, PrequalScores,
Expand All @@ -70,7 +71,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 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)))))))))))))))))))))))))))))))))))))))))))) {
export class ApiClient extends withDesignPerformance(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 @@ -214,18 +215,6 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
editors: { user: string; seconds_ago: number; viewpoint: unknown }[]; editor_count: number;
}>(`/projects/${pid}/collab`);
}
/** Embodied-carbon compliance: element totals, coverage and intensity against the project's limits. */
carbonComplianceReport(pid: string) {
return this.json<{
elements: { total_tco2e: number; coverage_pct: number; intensity_kgco2e_m2?: number;
carbon_matched: number; with_quantity: number;
hotspots: { guid: string; name: string | null; category: string; kgco2e: number }[] };
buy_clean: { rows: { category: string; achieved_factor: number; limit: number; unit: string;
pass: boolean; headroom_pct: number; action: string | null }[];
passing: number; failing: number };
leed_inventory: { total_tco2e: number; items: { category: string; kgco2e: number; share_pct: number }[] };
}>(`/projects/${pid}/carbon/compliance`);
}
/** PERMIT-CHECK: submission-readiness — checklist + ranked deficiencies + verdict (409 without a model). */
permitReadiness(pid: string) {
return this.json<{
Expand Down Expand Up @@ -341,9 +330,6 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
validate(pid: string) {
return fetch(this.url(`/projects/${pid}/validate`), { method: "POST" }).then((r) => r.json() as Promise<ValidationResult>);
}
energy(pid: string) {
return this.json<EnergyResult>(`/projects/${pid}/energy`);
}

// W9-1 property mapping / normalization — the transform verb between IDS-validate and COBie-export
propmapDetect(pid: string) {
Expand All @@ -354,20 +340,6 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
return this.json<{ dry_run: boolean; changed: number; rules: { from: string; to: string; matched: number; cast: string; keep_source: boolean; samples: { guid: string; from: string; to: string }[] }[] }>(
`/projects/${pid}/propmap/plan`, { method: "POST", body: JSON.stringify({ rules }) });
}
/** ENERGY phase 1 — the thermal model extracted from the IFC (zones · surfaces · constructions). */
energyModel(pid: string) {
return this.json<{ zone_source: string;
zones: { id: string; name: string; storey: string; area_m2: number; volume_m3: number }[];
surfaces: { id: string; name: string; ifc_class: string; idf_type: string; zone_id: string;
construction: string; orientation: string; area_m2: number; geometry: "exact" | "bbox";
corners: number[][] }[];
constructions: { name: string; u_value: number | null; source: string }[];
counts: Record<string, number>; note: string }>(`/projects/${pid}/energy/model`);
}
/** ENERGY phase 1 — the gbXML / IDF envelope export URLs (downloads, not JSON). */
energyExportUrl(pid: string, fmt: "gbxml" | "idf") {
return `${this.baseUrl}/projects/${pid}/energy/export.${fmt}`;
}
sharedParams(pid: string) {
return this.json<{ params: { name: string; pset: string; ptype: string; applies_to: string[];
label: string; description: string }[]; max: number }>(`/projects/${pid}/shared-params`);
Expand Down Expand Up @@ -399,12 +371,6 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
}

// --- portfolio benchmarking (cross-project) --------------------------------
benchmarkCosts(minSamples = 3) {
return this.json<{ cost_codes: { cost_code: string; samples: number; low: number; p25: number;
median: number; p75: number; high: number; total: number }[];
code_count: number; min_samples: number; codes_below_threshold: number; message?: string | null }>(
`/benchmarks/costs?min_samples=${minSamples}`);
}
benchmarkResponseRates() {
return this.json<{ rfi: { total: number; open: number; answered_or_closed: number;
avg_turnaround_days: number | null; overdue: number; overdue_pct: number };
Expand All @@ -428,11 +394,6 @@ export class ApiClient extends withDetailing(withAnnotate(withCreDeal(withClient
total_lien_exposure: number; vendors_at_risk: string[]; message?: string | null }>(
`/projects/${pid}/payapp/lien-exposure`);
}
projectCarbon(pid: string) {
return this.json<{ total_kgco2e: number; total_tco2e: number; line_count: number; unmatched: number;
by_material: Record<string, number>; by_cost_code: Record<string, number>; message?: string | null }>(
`/projects/${pid}/carbon`);
}
// --- design lifecycle (RIBA/AIA phases + itemized soft costs) ---------------
lifecycle(pid: string) {
return this.json<{ count: number; seeded: boolean;
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/api/cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ export function withCost<TBase extends Ctor<HttpCore>>(Base: TBase) {
adjustment: Record<string, unknown> | null;
}>(`/projects/${pid}/cost-vintage`);
}
/** Cross-project cost-code bands — the same question `unitRates` below answers at a different
* granularity (cost codes rather than unit rates), with the same low/p25/median/p75/high shape.
* It lives here and not in a `/benchmarks` file because this repo places those by what each one
* ANSWERS: `unitRates` was already here and `benchmarksPullPlanning` is in `schedule.ts`, both
* under the same route prefix. Grouping by the prefix would have contradicted two live placements. */
benchmarkCosts(minSamples = 3) {
return this.json<{ cost_codes: { cost_code: string; samples: number; low: number; p25: number;
median: number; p75: number; high: number; total: number }[];
code_count: number; min_samples: number; codes_below_threshold: number; message?: string | null }>(
`/benchmarks/costs?min_samples=${minSamples}`);
}
/** Actual unit rates per cost code across the caller's projects (cost ÷ installed quantity). */
unitRates(minProjects = 3) {
return this.json<{
Expand Down
69 changes: 69 additions & 0 deletions apps/web/src/api/designPerformance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/** What will this design CONSUME and EMIT, and does it comply?
*
* SCALE-SEAM ㉞. Five methods grouped by the question they answer, and the seam they sit on was
* drawn by earlier slices rather than by this one — which is why they belong together.
*
* **Design-phase PREDICTION, as against in-service MEASUREMENT.** `operations.ts` already holds the
* metered counterparts (`/projects/{pid}/energy/actual`, `/energy/benchmark-status`) and its own
* header records why the carbon half did not go with them: *"`projectCarbon` is EMBODIED carbon —
* what building it emits, a design-phase estimate. The GHG figures in `esgSummary` come from metered
* utility data. Same molecule, opposite ends of the asset life."* `models.ts` records the parallel
* call for energy — grouping by nearby comments *"would have dragged … `/energy` across the seam"*.
* So the axis was already committed to twice, independently, and these five are the prediction side
* of it: the thermal model and its gbXML/IDF handoff, and embodied carbon against the project target.
*
* **Deliberately NOT called `environmental.ts`.** That names the TOPIC, and the topic is exactly what
* both halves share — naming it that would re-blur the seam `operations.ts` drew. What separates them
* is not subject matter but whether the number is forecast or observed.
*
* A mixin, so every call site resolves unchanged. `api/surface.test.ts` is what proves it: moving a
* method is invisible to it, losing one fails it by number.
*/
import type { EnergyResult } from "./types";

import { HttpCore } from "./httpCore";

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

export function withDesignPerformance<TBase extends Ctor<HttpCore>>(Base: TBase) {
return class extends Base {
energy(pid: string) {
return this.json<EnergyResult>(`/projects/${pid}/energy`);
}

/** ENERGY phase 1 — the thermal model extracted from the IFC (zones · surfaces · constructions). */
energyModel(pid: string) {
return this.json<{ zone_source: string;
zones: { id: string; name: string; storey: string; area_m2: number; volume_m3: number }[];
surfaces: { id: string; name: string; ifc_class: string; idf_type: string; zone_id: string;
construction: string; orientation: string; area_m2: number; geometry: "exact" | "bbox";
corners: number[][] }[];
constructions: { name: string; u_value: number | null; source: string }[];
counts: Record<string, number>; note: string }>(`/projects/${pid}/energy/model`);
}

/** ENERGY phase 1 — the gbXML / IDF envelope export URLs (downloads, not JSON). */
energyExportUrl(pid: string, fmt: "gbxml" | "idf") {
return `${this.baseUrl}/projects/${pid}/energy/export.${fmt}`;
}

/** Embodied-carbon compliance: element totals, coverage and intensity against the project's limits. */
carbonComplianceReport(pid: string) {
return this.json<{
elements: { total_tco2e: number; coverage_pct: number; intensity_kgco2e_m2?: number;
carbon_matched: number; with_quantity: number;
hotspots: { guid: string; name: string | null; category: string; kgco2e: number }[] };
buy_clean: { rows: { category: string; achieved_factor: number; limit: number; unit: string;
pass: boolean; headroom_pct: number; action: string | null }[];
passing: number; failing: number };
leed_inventory: { total_tco2e: number; items: { category: string; kgco2e: number; share_pct: number }[] };
}>(`/projects/${pid}/carbon/compliance`);
}

projectCarbon(pid: string) {
return this.json<{ total_kgco2e: number; total_tco2e: number; line_count: number; unmatched: number;
by_material: Record<string, number>; by_cost_code: Record<string, number>; message?: string | null }>(
`/projects/${pid}/carbon`);
}
};
}
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ exact failure `roadmapLanes.test.ts` documents in its `MARKS` note. The gates ca
needs to keep being interleaved. **Re-measure the ceiling before ever promoting it again** — that
is the specific error row 2 made.
* **Not the next SCALE-SEAM slice.** ㉘ is genuinely next in a series that has shipped twenty-six
increments, but the series is now cutting into `client.ts` at **642** lines from a 3,600-odd start
increments, but the series is now cutting into `client.ts` at **603** lines from a 3,600-odd start
*(re-derived 2026-09-04; this read "2,837" — 4.4x the real figure — because the number was copied
forward through every slice since, which is the exact drift the rows above document twice)*. The marginal slice
is worth less than it was. *(㉘ also needed `MARKS` widened; that shipped in v0.3.1112.)* The vocabulary lives in
Expand Down Expand Up @@ -3181,7 +3181,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)–(100) took sixty-six of those 126 — 60 remain, and there is STILL no map.** A new
**(88)–(101) took seventy-two of those 126 — 54 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
2 changes: 1 addition & 1 deletion services/api/test_file_sizes.py

Large diffs are not rendered by default.