Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@ 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 — Portfolio resourcing

`GET /portfolio/resourcing` (`resource_portfolio.py`) sums weekly **concurrent** resource demand per
trade across projects. `?cap=` flags weeks where one trade is over-committed across the book and
names the competing projects. Rendered on Portfolio, with ⇄ marking the trades on more than one
project — the only ones that can be double-booked.

**A trade on three jobs in the same week looks comfortable on every one of them.** That is what a
per-project histogram cannot show, and it is the whole reason for the endpoint.
`test_resource_portfolio.py` proves it rather than asserting it: two projects at 6 units each are
each under a cap of 8, verified by calling their own `/schedule/resource-loading?cap=8` and getting
nothing back, while the book reports 12 over the same cap.

**"By department" turned out to be the wrong shape.** `resource_assignment.trade` is labelled
"Trade / discipline", and no `department` field exists anywhere in the backend. A department axis is
a product decision, not a filter over data we hold — raised in the roadmap rather than invented.

**Fidelity is reported, not blended.** A project with no assignments falls back to activity
`crew_size` — a crew count, not a resourced plan — so every row carries its `source` and `fidelity`
gives the split.

The two `over_allocation` shapes are **not** interchangeable and both docstrings say so: this one
caps per trade across the book; `resource_loading`'s caps one project's total weekly units.

Found on the way in: the field named `resourced` made `test_route_reachability` report
`/schedule/eot/sourced` as called, because that gate matches route leaves as substrings and
`resourced` contains `sourced`. Renamed to `assigned`; the second instance of that collision is
recorded in the gate's own notes.

## Unreleased — Cross-project Gantt

The Programme card (`/projects/{pid}/schedule/portfolio`) now draws a bar per project on a shared
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/api/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,37 @@ export function withSchedule<TBase extends Ctor<HttpCore>>(Base: TBase) {
over_allocation: { week: string; units: number; cap: number | null }[]; note: string }>(
`/projects/${pid}/schedule/resource-loading${cap != null ? `?cap=${cap}` : ""}`);
}
/** The same weekly demand summed **across** projects — R22-PIPELINE's portfolio resourcing axis.
*
* `resourceLoading` above answers one project, and a trade committed to three jobs in the same
* week looks comfortable on every one of them. Note the two `over_allocation` shapes are NOT
* interchangeable: this one caps **per trade** across the book and names the competing projects,
* while `resourceLoading`'s caps a single project's **total** weekly units.
*
* `fidelity` is not decoration — a project with no resource assignments falls back to activity
* `crew_size`, which is a crew count rather than a resourced plan, and a book of fallbacks must
* not read as a resourced one. */
portfolioResourcing(opts: { cap?: number; limit?: number; weeks?: number } = {}) {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(opts)) if (v != null) q.set(k, String(v));
return this.json<{
available: boolean; reason?: string;
projects: { id: string; name: string; source: string; loads: number; trades: string[];
unit_weeks: number; cost: number }[];
projects_without_loads: { id: string; name: string; reason: string }[];
trades: { trade: string; peak_units: number; peak_week: string | null; unit_weeks: number;
cost: number; project_count: number; cross_project: boolean }[];
weeks: { week: string; total: number; by_trade: Record<string, number> }[];
week_span?: { start: string; finish: string; count: number; shown: number };
peak: { week: string; units: number } | null;
over_allocation: { week: string; trade: string; units: number; cap: number;
projects: Record<string, number> }[];
cap: number | null;
fidelity: { by_source: Record<string, number>; assigned: number; fallback: number;
note?: string };
project_count: number; projects_available: number; truncated: boolean; note?: string;
}>(`/portfolio/resourcing${q.toString() ? `?${q}` : ""}`);
}
/** Resource-leveling advisory: over-allocated work with CPM float that can be smoothed within float. */
resourceLeveling(pid: string, cap: number) {
return this.json<{ cap: number; peak: { week: string | null; units: number }; over_weeks: number;
Expand Down
49 changes: 49 additions & 0 deletions apps/web/src/portal/panels/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,55 @@ export async function renderPortfolio(ctx: PanelContext) {
ctx.root.appendChild(card);
}).catch(() => { /* returns spread is best-effort; the roll-up above stands on its own */ });

// RESOURCING ACROSS THE BOOK — R22-PIPELINE's last item. The per-project resource histogram
// already exists; what it cannot show is a trade committed to three jobs in the same week,
// because that trade looks comfortable on every one of them. Same shape as the cross-project
// Gantt: the thing only visible once you sum.
//
// `trade` is the dimension the schema carries (`resource_assignment.trade`, labelled
// "Trade / discipline"). There is no department field anywhere, so department reporting is a
// product decision rather than a filter — recorded in the roadmap, not invented here.
void ctx.host.api.portfolioResourcing().then((rp) => {
if (!rp.available || !rp.trades.length) return;
const card = document.createElement("div"); card.className = "dash-card"; card.style.marginTop = "10px";
const f = rp.fidelity;
card.innerHTML = `<b>Resourcing across the book</b> <span class="meta">`
+ `${rp.projects.length} project(s) · ${rp.trades.length} trade(s)`
+ (rp.peak ? ` · peak ${rp.peak.units} concurrent units in ${esc(rp.peak.week)}` : "")
+ (f.fallback ? ` · ${f.assigned} assigned / ${f.fallback} from crew counts` : "")
+ (rp.truncated ? ` · showing ${rp.project_count} of ${rp.projects_available}` : "")
+ `</span>`;
const tbl = document.createElement("table"); tbl.className = "portal-table"; tbl.style.fontSize = "11px";
tbl.innerHTML = `<thead><tr><th scope="col">Trade / discipline</th>`
+ `<th scope="col" style="text-align:right">Peak</th><th scope="col">Peak week</th>`
+ `<th scope="col" style="text-align:right">Projects</th>`
+ `<th scope="col" style="text-align:right">Unit-weeks</th></tr></thead>`;
const tb = document.createElement("tbody");
for (const t of rp.trades) {
const tr = document.createElement("tr");
// A trade on more than one project is the only kind that CAN be double-booked, so it is
// the only kind worth colouring — this is a fact from the data, not a severity guess.
const col = t.cross_project ? "var(--status-warn)" : "var(--muted)";
tr.innerHTML = `<td>${esc(t.trade)}${t.cross_project ? ` <span style="color:${col}" title="on ${t.project_count} projects — can be double-booked">⇄</span>` : ""}</td>`
+ `<td style="text-align:right;font-weight:600">${t.peak_units}</td>`
+ `<td class="meta">${esc(t.peak_week ?? "—")}</td>`
+ `<td style="text-align:right">${t.project_count}</td>`
+ `<td style="text-align:right">${t.unit_weeks}</td>`;
tb.appendChild(tr);
}
tbl.appendChild(tb); card.appendChild(tbl);
if (rp.projects_without_loads.length) {
const u = document.createElement("div"); u.className = "meta"; u.style.marginTop = "4px";
u.textContent = "No resourcing data — " + rp.projects_without_loads
.map((x) => esc(x.name)).join(", ");
card.appendChild(u);
}
card.appendChild(Object.assign(document.createElement("div"), { className: "meta",
textContent: "⇄ marks a trade committed to more than one project — the only kind that can be "
+ "double-booked. Peak is concurrent units summed across the book in its busiest week." }));
ctx.root.appendChild(card);
}).catch(() => { /* resourcing is best-effort — no assignments in this deployment */ });

// RISK HEAT MAP — R22-PIPELINE. The table above says how each project is PERFORMING; this says
// which risk ENGINE is hot on which project, which is the question that decides where a
// programme director spends the morning. Cells come from the same `risk_board` each project's
Expand Down
32 changes: 29 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -1910,9 +1910,35 @@ stakes we are missing.

*Cost of the premise-check: one grep. Cost of believing the entry: a scheduling engine.*

**Still open: department resourcing.** The resourcing half is not the small
item this entry's phrasing suggests — `resource_loading.py` groups by **trade and resource type**,
per project, so "by department" needs both a new dimension and a portfolio axis. Size it on its own.
✅ **Portfolio resourcing SHIPPED — `GET /portfolio/resourcing`,
`services/api/src/aec_api/resource_portfolio.py`.** Weekly CONCURRENT demand per trade, summed
across the book. `?cap=` flags the weeks where one trade is over-committed **across projects** and
names which projects are competing for it. `services/api/test_resource_portfolio.py` pins the
claim that only a cross-project view can make: two projects at 6 units each are both under a cap
of 8, and together they are not — asserted by calling each project's own
`/schedule/resource-loading?cap=8` and confirming it reports nothing.

**"By department" was the wrong shape, and the schema says so.** `resource_assignment.trade` is
labelled **"Trade / discipline"**, and the word "department" appears nowhere in the backend except
a comment in `rooms.py` and a fire-department scope clause. So a department axis is **a product
decision** — what is a department that a trade is not? field-vs-office for a GC, or
Architecture / Structural / MEP for a design firm — **not a filter over data we hold.** Raised
rather than invented: a dimension nobody has defined cannot be reported honestly. **The portfolio
axis was the half that mattered and it needed no new field.**

**Fidelity is reported, not blended.** `resource_loading` falls back to
`schedule_activity.crew_size` when a project has no assignments; that is a crew count, not a
resourced plan. Every project row carries its `source` and `fidelity` gives the split, so a book
of fallbacks cannot read as a resourced one — the heat map's rule one step along: *do not let a
lower-fidelity value wear the costume of a higher-fidelity one.*

⚠️ **A gate caught something on the way in, and it was a WORD.** Adding this put the field
`fidelity.resourced` into the web source, and `sourced` is the leaf of `/schedule/eot/sourced`, so
`test_route_reachability` reported that frozen-uncalled route as called. Renamed to `assigned`.
The second instance of a class that file already records; the note there explains why the matcher
is not the thing to change.

**R22-PIPELINE is now closed apart from the department question above, which is the user's.**
## ⚡ R23 — ENGINEERING UPGRADE RING *(technical scan 2026-07-25; file:line evidence)*

**A THIRD false blocker, and the biggest one.** **W10-9 dimensional constraints** has sat gated for
Expand Down
Loading