Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
276c4e5
Merge after the #437 squash
claude Sep 4, 2026
ea414ab
Portfolio risk heat map — R22-PIPELINE
claude Sep 4, 2026
9ba106e
Review round on #439 — deterministic tie-break, keyboard-operable rows
claude Sep 4, 2026
4ae3e28
Merge remote-tracking branch 'origin/main' into claude/codebase-audit…
claude Sep 4, 2026
27d6f40
Cross-project Gantt — R22-PIPELINE
claude Sep 4, 2026
3f07a03
Review round on #440 — reject normalised dates, require the id separa…
claude Sep 4, 2026
285f262
Merge remote-tracking branch 'origin/main' into claude/codebase-audit…
claude Sep 4, 2026
09a2446
Portfolio resourcing — R22-PIPELINE's last item
claude Sep 4, 2026
b614e0b
Merge remote-tracking branch 'origin/main' into claude/codebase-audit…
claude Sep 4, 2026
fadc11d
Roadmap truth pass + R39-DECOMP-VIEWER (17)
claude Sep 5, 2026
8c9314d
SCALE-SEAM (102) — counterparty risk, client.ts 603 -> 589
claude Sep 5, 2026
c7a58ae
Review fixes on #442, and a gate for the number that keeps drifting
claude Sep 5, 2026
a2b33eb
Merge remote-tracking branch 'origin/main' into claude/codebase-audit…
claude Sep 5, 2026
e1f1a05
SCALE-SEAM (103) — acceptance gates, client.ts 589 -> 572
claude Sep 5, 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
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@ 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 (103): acceptance gates

Extracts `permitReadiness`, `diligenceReadiness`, `handoverAcceptance` and `validate` from
`client.ts` (**589 → 572**) into `apps/web/src/api/acceptanceGates.ts` — *will an outside party
accept this project, and what is blocking it.*

**Neither the route nor the audience produces this grouping.** The four routes are
`/permit/readiness`, `/diligence/readiness`, `/handover/acceptance` and `/validate` — four different
prefixes, so a prefix grouping takes one each and the set never forms; the shared leaf word
"readiness" reaches only two of the four. The deciding parties are an AHJ, an investor, an owner and
an IDS checker — four different audiences, so grouping by reader fails too. What forms it is the
**shape of the return**: each collapses the whole project to a single accept/refuse verdict
(`verdict` / `go` / `accepted` / `status: "pass" | "fail"`) and then enumerates what withholds it.

(102) was carried by a prefix that actively *disagreed* with the seam; this one by a vocabulary that
says nothing at all — **four names sharing no words can still be one question**. That is the inverse
of the error (85), (89) and `annotate.ts` each recorded, where a shared mechanism looked like a
shared question.

**The two exclusions did the work.** `spineTraceability` is the closest miss — same domain,
adjacent in the file, equally project-scoped — and returns `coverage`/`gaps`/`chain` with **no
verdict field at all**: it maps completeness for a human rather than deciding acceptance.
`editPrecheck` *does* return a verdict, but judges a **pending action** ("may I run this recipe with
these params"), is remedied by changing the parameters you are about to submit, and sits beside
`addCurtainWall` as the precheck for `editIfc`.

**One false positive is worth recording.** A scan for verdict-shaped returns also flagged
`collabSnapshot` — but the match came from the doc comment introducing `permitReadiness`, the *next*
method. A method-body splitter that runs to the next header swallows the comment belonging to that
header, so the population silently inherits its neighbour's vocabulary; a count of six looked
entirely checked. Only reading each candidate caught it.

Also files `handoverAcceptance`, which an earlier slice had parked under an explicit "UNFILED" note
asking for it to be placed *by what it answers rather than by what it sits next to*. That note is
**narrowed to two entries, not deleted** — the other two are still genuinely unfiled, and a note
that silently loses entries is how earlier slices lost methods.

## Unreleased — review fixes, and a gate for the number that keeps drifting

**Four review findings on the same pull request, and the second one was self-referential.** The PR
Expand Down
118 changes: 118 additions & 0 deletions apps/web/src/api/acceptanceGates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { HttpCore } from "./httpCore";
import type { DiligenceReadiness } from "./types";

/**
* Acceptance gates — **will an outside party accept this project, and what is blocking it.**
*
* SCALE-SEAM (103). Four gates, four different outside parties, one question shape: a single
* whole-project verdict plus the itemised list of what is standing in its way.
*
* | method | who is deciding | the verdict field | what blocks it |
* |---|---|---|---|
* | `permitReadiness` | the AHJ | `verdict` | `checklist[].satisfied` + ranked `deficiencies[]` |
* | `diligenceReadiness` | an investor / acquirer | `go` | `high_risk[]` + `flagged` per category |
* | `handoverAcceptance` | the owner at turnover | `accepted` | `checks[].ok` |
* | `validate` | the IDS checker | `status: "pass" \| "fail"` | `specifications[].failed_guids` |
*
* ## The witness is that NEITHER the route nor the audience produces this grouping
*
* The four routes are `/permit/readiness`, `/diligence/readiness`, `/handover/acceptance` and
* `/validate` — **four different prefixes**, so a prefix grouping takes one each and this set never
* forms. The shared leaf word "readiness" reaches only two of the four, so that does not form it
* either. And the deciding parties are an authority, an investor, an owner and a schema checker —
* **four different audiences**, so grouping by who reads it fails as well.
*
* What does form it is the *shape of the return*: every one of them collapses the whole project to
* a single accept/refuse verdict and then enumerates what is withholding it. That is a question a
* caller acts on identically in all four cases — block, or proceed — and the remedy is always to
* change the project and ask again.
*
* This is the second slice to be carried by return shape rather than by name, after (102), where
* the prefix actively *disagreed* with the seam. Here the prefix does not disagree so much as say
* nothing at all, which is the weaker but more common case: **four names that share no vocabulary
* can still be one question.** (85) rejected "they are all multipart uploads", (89) "they are all
* module records", and `annotate.ts` "they all call `editIfc`" — those are shared mechanisms
* masquerading as questions. This is the inverse error to avoid: an unshared vocabulary
* masquerading as unshared subject matter.
*
* ## What did NOT come, and why — the exclusions are the load-bearing part
*
* **`spineTraceability` stayed**, and it is the closest miss: same domain, same "is this project
* complete" register, adjacent in the file, and its route `/spine/traceability` is as
* project-scoped as the four above. It returns `coverage` percentages, `gaps` and a `chain` —
* and **no verdict field of any kind**. Nothing in it says pass or fail, because it is not a gate:
* it maps how completely one artefact links to another so a human can decide what to do. A
* completeness map and an acceptance decision are different questions even when they cover
* identical ground, and only reading the returns tells them apart.
*
* **`editPrecheck` stayed**, and this one is subtler because it genuinely returns a verdict —
* `{ok, errors, warnings}`. Its SUBJECT is different: it judges a **pending action** ("may I run
* this recipe with these params"), asked before acting, remedied by changing the parameters you
* are about to submit, and consumed by enabling or disabling an Apply button. The four above judge
* a **delivered state**, remedied by changing the project. It also sits beside `addCurtainWall` as
* the precheck for `editIfc`, so moving it here would separate it from the thing it prechecks.
* *A verdict about what you are about to do is not a verdict about what you have built.*
*
* **`collabSnapshot` was a false positive and is worth recording.** A scan for verdict-shaped
* returns flagged it, but the match came from the doc comment introducing `permitReadiness` — the
* next method — not from its own body, which has no verdict at all. A method-body splitter that
* runs to the next header swallows the comment belonging to that header, so a population derived
* that way silently inherits its neighbour's vocabulary. The count looked entirely reasonable at
* six; only reading each candidate caught it. **Derive the complement, then read it — a plausible
* count is not a checked one.**
*
* ## `handoverAcceptance` had been left explicitly unfiled
*
* A previous slice parked it under an "UNFILED" note in `client.ts` saying it "needs its home
* decided by what it ANSWERS rather than by what it sits next to". This slice is that decision.
* The note is narrowed rather than deleted, because the other two it names are still unfiled.
*/
// The mixin shape TS requires — same declaration as every other `with*` module in this directory.
type Ctor<T> = new (...args: any[]) => T;

export function withAcceptanceGates<TBase extends Ctor<HttpCore>>(Base: TBase) {
return class AcceptanceGates extends Base {
/** PERMIT-CHECK: submission-readiness — checklist + ranked deficiencies + verdict (409 without a model). */
permitReadiness(pid: string) {
return this.json<{
verdict: string; readiness_pct: number; approvability_score: number;
checklist: { requirement: string; satisfied: boolean; evidence: string }[];
deficiencies: { item: string; severity: string; action: string }[];
}>(`/projects/${pid}/permit/readiness`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/** Investor/acquirer gate: `go` plus the diligence items and entitlements holding it back. */
diligenceReadiness(pid: string) {
return this.json<DiligenceReadiness>(`/projects/${pid}/diligence/readiness`);
}

/** Owner turnover gate: `accepted` plus the per-check breakdown of what is not ready. */
handoverAcceptance(pid: string) {
return this.json<{ accepted: boolean; checks: { key: string; label: string; ok: boolean }[];
metrics: Record<string, number>; note: string }>(`/projects/${pid}/handover/acceptance`);
}

/**
* IDS gate: `status` plus, per specification, the GUIDs that failed it.
*
* Uses bare `fetch` rather than `this.json` — kept exactly as it was, because changing the
* transport of a method while moving it makes a behavioural change look like an extraction.
*/
validate(pid: string) {
return fetch(this.url(`/projects/${pid}/validate`), { method: "POST" })
.then((r) => r.json() as Promise<ValidationResult>);
}
};
}

/**
* The IDS validation report. Defined here rather than in `types.ts` because `validate` is its only
* consumer in the tree — it moved out of `client.ts` with the method that returns it, so nothing is
* left behind pointing at a method that is no longer there.
*/
export interface ValidationResult {
title: string;
status: "pass" | "fail";
summary: { specifications: number; passed: number; failed: number };
specifications: { name: string; status: "pass" | "fail"; applicable: number; passed: number; failed: number; failed_guids: string[] }[];
}
45 changes: 14 additions & 31 deletions apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { withCost } from "./cost";
import { withRoutines } from "./routines";
import { withContracts } from "./contracts";
import { withCounterpartyRisk } from "./counterpartyRisk";
import { withAcceptanceGates } from "./acceptanceGates";
import { withDesignOptions } from "./designOptions";
import { withFinance } from "./finance";
import { withLibrary } from "./library";
Expand Down Expand Up @@ -66,13 +67,13 @@ import type {
DisciplineTree, ModulePin, RoomAllocation,
PropMapRule,
SpecManual, WorkItem, VitalsPayload,
DiligenceReadiness, MasterBuilderBrief,
MasterBuilderBrief,
SpineTraceability } from "./types";


// 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 withCounterpartyRisk(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)))))))))))))))))))))))))))))))))))))))))))))) {
export class ApiClient extends withAcceptanceGates(withCounterpartyRisk(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 @@ -216,14 +217,6 @@ export class ApiClient extends withCounterpartyRisk(withDesignPerformance(withDe
editors: { user: string; seconds_ago: number; viewpoint: unknown }[]; editor_count: number;
}>(`/projects/${pid}/collab`);
}
/** PERMIT-CHECK: submission-readiness — checklist + ranked deficiencies + verdict (409 without a model). */
permitReadiness(pid: string) {
return this.json<{
verdict: string; readiness_pct: number; approvability_score: number;
checklist: { requirement: string; satisfied: boolean; evidence: string }[];
deficiencies: { item: string; severity: string; action: string }[];
}>(`/projects/${pid}/permit/readiness`);
}


/** Discipline quantity roll-up — reinforcement tonnage, MEP linear runs, structural volume. */
Expand Down Expand Up @@ -328,9 +321,6 @@ export class ApiClient extends withCounterpartyRisk(withDesignPerformance(withDe
mean_deviation: number; max_deviation: number; p95_deviation: number;
histogram: { band: string; count: number }[]; note: string }>;
}
validate(pid: string) {
return fetch(this.url(`/projects/${pid}/validate`), { method: "POST" }).then((r) => r.json() as Promise<ValidationResult>);
}

// W9-1 property mapping / normalization — the transform verb between IDS-validate and COBie-export
propmapDetect(pid: string) {
Expand Down Expand Up @@ -395,9 +385,6 @@ export class ApiClient extends withCounterpartyRisk(withDesignPerformance(withDe
return this.json<{ seeded: boolean; phases?: number; reason?: string }>(
`/projects/${pid}/lifecycle/seed`, { method: "POST" });
}
diligenceReadiness(pid: string) {
return this.json<DiligenceReadiness>(`/projects/${pid}/diligence/readiness`);
}



Expand All @@ -419,19 +406,22 @@ export class ApiClient extends withCounterpartyRisk(withDesignPerformance(withDe
}


// --- UNFILED: three methods that the RACI banner above used to cover -----------------
// --- UNFILED: two methods that the RACI banner above used to cover -------------------
// Named rather than left implicit, because a banner that over-claims is how the previous
// three slices each lost a method. `mcpTools` is global (`/mcp/tools`); `handoverAcceptance`
// is `/handover/acceptance`; `inspectVim` is `/convert/vim/inspect`. None is RACI, and each
// needs its home decided by what it ANSWERS rather than by what it sits next to.
// three slices each lost a method. `mcpTools` is global (`/mcp/tools`); `inspectVim` is
// `/convert/vim/inspect`. Neither is RACI, and each still needs its home decided by what it
// ANSWERS rather than by what it sits next to.
//
// This note said THREE until SCALE-SEAM (103). `handoverAcceptance` was the third, and it is
// the note working as intended: it was parked here with the instruction to file it by what it
// answers, and (103) answered that — an owner's turnover gate, which is the same question as
// the AHJ's, the investor's and the IDS checker's. `acceptanceGates.ts` has it now. **Narrowed
// rather than deleted**, because the other two are still genuinely unfiled and a note that
// silently loses entries is how the earlier slices lost methods in the first place.
mcpTools() {
return this.json<{ tools: { name: string; description: string }[]; server: string; note: string }>(
`/mcp/tools`);
}
handoverAcceptance(pid: string) {
return this.json<{ accepted: boolean; checks: { key: string; label: string; ok: boolean }[];
metrics: Record<string, number>; note: string }>(`/projects/${pid}/handover/acceptance`);
}

async inspectVim(file: File) {
const fd = new FormData(); fd.append("file", file);
Expand Down Expand Up @@ -580,10 +570,3 @@ export class ApiClient extends withCounterpartyRisk(withDesignPerformance(withDe
}
}


export interface ValidationResult {
title: string;
status: "pass" | "fail";
summary: { specifications: number; passed: number; failed: number };
specifications: { name: string; status: "pass" | "fail"; applicable: number; passed: number; failed: number; failed_guids: string[] }[];
}
2 changes: 1 addition & 1 deletion services/api/test_file_sizes.py

Large diffs are not rendered by default.