Skip to content

aggregator(weights): effortWeight's plain-object lookup returns prototype members as weights — 'effort/constructor' counts as sized and string-poisons every weighted summary #2734

Closed

Description

@dezren39

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;         // :38

EFFORT_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] } (typeof function), not null.
  • effortWeight(["effort/__proto__"]) → Object.prototype (typeof object), not null.
  • 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"]}]) → totalWeight and closedWeight become the string "0function Object() { [native code] }1" (number += function coerces to concatenation), weightedPct is null while unsizedCount stays 0 — 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

  • effortWeight returns null for every effort/* suffix that is not one of the four defined sizes — including effort/constructor and effort/__proto__ — so such items count as unsized.
  • summarizeWeights over items carrying those labels produces numeric totalWeight/closedWeight/openWeight and the correct unsizedCount; weightedLine output 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    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.hasOwn at 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.

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  5. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

    • Agent: queue worker (repo-worker-code), lane B shift
    • Session / ID: queue task 4077
    • Found via: queue claim (fleet-monitor safety-net packet)
    • Task: Triage of the effortWeight prototype-lookup bug — verdict: CONFIRMED, exactly as filed.
    • Findings:
      • Re-verified on current main b5d334b0b6de (2026-10-31): pm-stack/aggregator/src/weights.ts still defines EFFORT_WEIGHTS as a plain object literal and effortWeight still does const w = EFFORT_WEIGHTS[size]; if (w !== undefined) return w; with no own-key check — so effort/constructor and effort/__proto__ resolve through the prototype chain and are returned as "weights", breaking the module's own unsized contract (weights.ts header, lines 9–13).
      • The creator's real-module probe on ec02aa14 (issue body) stands unrebutted: constructor → the Object function, __proto__ → Object.prototype, summarizeWeights totals string-poisoned to "0function Object() { [native code] }1" with unsizedCount stuck at 0; controls behave. The code path between that main and today's is byte-identical in the relevant lines.
      • Fresh checks: no PR addresses this; EFFORT_WEIGHTS is referenced only inside weights.ts (code search), so the fix is contained; test/weights.test.ts exists as the regression-test home.
    • Triage pin for the fix shape: guard the lookup with 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).
    • Outcome: Fix follow-up enqueued as queue task 4095 (small_fix, packet packet-2734-fix.json) carrying the issue's acceptance criteria verbatim plus the pin.
  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Oct 31, 2026
  8. added a commit that references this issue on Oct 31, 2026
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Fixed by #2884 (merged e0d6d49).

    🤖 Agent note · done (fix merged)

  10. added
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions