Repository navigation
Description
Problem
effortWeight (pm-stack/aggregator/src/weights.ts:33-44) resolves an effort/* label through a plain-object lookup with no own-key check:
export const EFFORT_WEIGHTS: Readonly<Record<string, number>> = { xs: 1, s: 4, m: 12, l: 40 }; // :19
const size = label.slice("effort/".length);
const w = EFFORT_WEIGHTS[size]; // :37
if (w !== undefined) return w; // :38EFFORT_WEIGHTS is an object literal, so the lookup walks the prototype chain. Label text is lowercased before the lookup (:36), which happens to shield the camelCase members (toString, hasOwnProperty, valueOf), but the all-lowercase inherited keys still hit: effort/constructor resolves to the Object constructor function and effort/__proto__ resolves to Object.prototype — both !== undefined, so both are returned as the item's "weight" instead of null (unsized).
That breaks the module's own contract (weights.ts:9-13): an item with no recognised effort label is UNSIZED and must be counted separately, "never silently zero-weighted". An item carrying one of these labels is instead counted as sized with a non-numeric weight.
Verified this run (laptop probe, real module, main ec02aa1)
Probe: laptop ~/git/fleet/probe-2026-10-30-laneA-weights/probe.ts (Bun, imports the real weights.ts, copied byte-for-byte from main ec02aa1; md5 9705f795f0a638f2b90a4f98aa432268):
effortWeight(["effort/constructor"])→function Object() { [native code] }(typeoffunction), notnull.effortWeight(["effort/__proto__"])→Object.prototype(typeofobject), notnull.- Controls behave:
effortWeight(["effort/xs"])→1;effort/toString/effort/hasOwnProperty→null(lowercasing shields them, as above). summarizeWeights([{state:"closed", labels:["effort/constructor"]}, {state:"closed", labels:["effort/xs"]}])→totalWeightandclosedWeightbecome the string"0function Object() { [native code] }1"(number+=function coerces to concatenation),weightedPctisnullwhileunsizedCountstays0— the summary claims everything is sized and nothing is weighted.weightedLine(...)then renders the function source into the dashboard text:— (nothing sized) (0function Object() { [native code] }1/0function Object() { [native code] }1 weight closed) · 2/2 items closed.
Why it matters
summarizeWeights feeds the project page, the /projects per-Area rows and the /program cards (callers: render.ts:4873, :4877, :5090, :5092; project-config.ts:763; effortWeight also directly at render.ts:5095). Pool item labels come from the org Project / GitHub issues — label names are data, not code, and the taxonomy explicitly encourages coining labels freely, so an effort/* label outside the four known sizes is a when, not an if (a typo'd or experimental label is all it takes; it does not need to be one of the two prototype names — those are just the ones that collide today). Any unrecognised effort/* label should fall through to unsized; two of them instead corrupt every weighted summary on the page into string-typed weights and a nonsense progress line, with no error anywhere. The fix shape (own-key check, Object.hasOwn, a Map, or a null-prototype record) is triage's pin; silently treating a prototype member as a weight is the one outcome that is wrong either way.
Acceptance criteria
-
effortWeightreturnsnullfor everyeffort/*suffix that is not one of the four defined sizes — includingeffort/constructorandeffort/__proto__— so such items count as unsized. -
summarizeWeightsover items carrying those labels produces numerictotalWeight/closedWeight/openWeightand the correctunsizedCount;weightedLineoutput contains no function/object source text. - Regression tests in pm-stack/aggregator/test/weights.test.ts cover at least:
effort/constructor,effort/__proto__, and one ordinary unknown size (e.g.effort/xl) — all unsized. - The four real sizes (xs/s/m/l) resolve exactly as before, including the first-recognised-wins rule for multiple
effort/*labels, and the existing weights tests still pass.
Dedup notes
Searched open + closed for weights, effortWeight, summarizeWeights, constructor, proto: no existing issue touches this lookup. #1108 is the feature that introduced the weights module; the aggregator validation findings (#2576 family) cover feed/gatherer inputs, not this label lookup.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 30, 2026
agent-note:v1 — design-architect pass 21 (2026-10-30): this issue is ADOPTED as part of Phase 2 of design #2792 ("own keys only — a plain object is not a map for untrusted keys", doc
files/designs/code-own-key-lookups.md). It is linked as a sub-issue of #2792. Triage: do NOT re-file, re-scope, or split — record the phase structure instead. Phase decisions recorded on #2792: the fix is the own-key rule (Object.hasOwnat the read site — the #2787 embodiment, closed today in id/web); a prototype spelling must behave exactly like a genuinely unknown key (here:null, unsized). Phase 2 pairs this issue with #2781 (same app, same fix idiom, shared probe matrix as tests). Also settled on #2792: this family is NOT in #2576's scope — that design validates document contents; lookup semantics are a different mechanism.dezren39 commentedon Oct 31, 2026
🤖 Agent note · done (triage)
effortWeightprototype-lookup bug — verdict: CONFIRMED, exactly as filed.b5d334b0b6de(2026-10-31):pm-stack/aggregator/src/weights.tsstill definesEFFORT_WEIGHTSas a plain object literal andeffortWeightstill doesconst w = EFFORT_WEIGHTS[size]; if (w !== undefined) return w;with no own-key check — soeffort/constructorandeffort/__proto__resolve through the prototype chain and are returned as "weights", breaking the module's own unsized contract (weights.ts header, lines 9–13).ec02aa14(issue body) stands unrebutted: constructor → theObjectfunction,__proto__→Object.prototype,summarizeWeightstotals string-poisoned to"0function Object() { [native code] }1"withunsizedCountstuck at 0; controls behave. The code path between that main and today's is byte-identical in the relevant lines.EFFORT_WEIGHTSis referenced only insideweights.ts(code search), so the fix is contained;test/weights.test.tsexists as the regression-test home.Object.hasOwn(EFFORT_WEIGHTS, size)— the minimal change that keeps the exported record shape and first-recognised-wins rule untouched (a Map or null-prototype record would also work; hasOwn is the smallest diff).packet-2734-fix.json) carrying the issue's acceptance criteria verbatim plus the pin.dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
effortWeightwith an own-key check (Object.hasOwn) + regression tests intest/weights.test.ts.Merge pull request #2884 from developing-today/queue/4095-2734-effort…
dezren39 commentedon Oct 31, 2026
Fixed by #2884 (merged
e0d6d49).🤖 Agent note · done (fix merged)
effortWeight+ regression tests; merged via merge commit per repo style. No PR-triggered CI exists forpm-stack/**paths (verified: only CodeRabbit reported, NEUTRAL); standing criteria met via local verification —bun test test/weights.test.ts11/11 pass, new tests fail on unfixed code, full suite baseline-identical to clean main (7 pre-existing page-manifest failures).