Repository navigation
Description
Summary
One rule for the write side of the prototype family: a value keyed by an externally-spellable name is stored with define semantics, never assignment semantics. Assignment (obj[key] = v, obj[key] ??= …, dst[k] = src[k] copy loops, Object.assign(dst, src)) goes through [[Set]], and for exactly one spelling — __proto__ — [[Set]] invokes the Object.prototype setter instead of creating an own property: a non-object value is silently ignored, an object value replaces the container's prototype. Three filed instances in three surfaces prove the class; this design adopts them as its phases and closes with an inventory audit.
This is the write-side counterpart of #2792 (read-side own-key lookups), whose design doc explicitly scoped this class out ("a different bug class with a different fix… a future design decides") and whose pass-23 assessment recorded the trigger: originate when a second write-side instance files after #2851. There are now three.
Design doc: files/designs/code-proto-write-side.md in the keep-the-four-repos-healthy goal workspace (problem, evidence, alternatives, risks, open questions).
The instances (audited at origin/main 106b0571)
- aggregator(snapshots): SnapshotStore groups by serviceId into plain objects — a '__proto__'/'constructor'/'toString' serviceId crashes byService (blanking all snapshot history) and pollutes Object.prototype during compact #2941 — aggregator snapshots (Phase 1, the polluting instance).
SnapshotStore.byService()(pm-stack/aggregator/src/snapshots.ts:562-565) groups with(grouped[r.serviceId] ??= []).push(r)andcompactInner()(:703-711) accumulates its report the same way;serviceIdis validated only as a non-empty string (:303) and arrives from external feed jobs. A__proto__/constructor/toStringid makesbyService()throw (its only caller swallows the throw into{}— all snapshot history blanks, persistently, since the record passes validation and is stored), andcompact()with a__proto__id writes its counts ontoObject.prototypeitself (({}).before === NaNafterwards). The same file already groups withMapatappendLocked(~:620) and incompactRecordsToRollups— these two sites are outliers in their own file. - aggregator(cost-explorer): a setup named '__proto__' is silently never saved — save/import assign into a plain object, so the name becomes the store's prototype instead of an entry #2851 — aggregator cost-explorer (Phase 2). Saved setups live in a plain object from localStorage (
pm-stack/aggregator/src/render.ts:4338saveall[name] = …, :4359 importall[k] = parsed[k]). A setup named__proto__is silently never saved; an imported one mutates the store object's prototype. Silent data loss both directions. - record(loose): a '__proto__' field in loose text is silently dropped — parseLoose assigns into a plain object, so the field never reaches the JSONL line (the JSON path preserves it) #2828 — bin/record (Phase 2).
parseLoose(bin/record.mjs:110) builds fields by assignment; a loose__proto__field counts toward the keyed-segment threshold but never reaches the appended JSONL line — breaking record's never-lose-a-field contract for exactly the key an agent records while reporting this bug class. The JSON path preserves it (parse + spread = define semantics); the two paths disagree.
Decisions recorded
- Preserve, don't reject. The key is data: a
__proto__field / setup name / serviceId is stored, listed, exported and re-read like any other key. (aggregator(cost-explorer): a setup named '__proto__' is silently never saved — save/import assign into a plain object, so the name becomes the store's prototype instead of an entry #2851's acceptance permits visible rejection for setup names; this design records preserve so all surfaces behave identically and aggregator(snapshots): SnapshotStore groups by serviceId into plain objects — a '__proto__'/'constructor'/'toString' serviceId crashes byService (blanking all snapshot history) and pollutes Object.prototype during compact #2941's already-stored records become readable instead of stranded. Validation-side rejection is also not the fix — aggregator(snapshots): SnapshotStore groups by serviceId into plain objects — a '__proto__'/'constructor'/'toString' serviceId crashes byService (blanking all snapshot history) and pollutes Object.prototype during compact #2941's acceptance already states the grouping fix must not depend on it.) - The prototype must not move — asserted as a test property: after any covered write, the container's prototype is unchanged (or
nullby design) andObject.prototypegains no own properties. - Accumulators are construction, not lookup. Grouping/counting sites are fixed by changing the container (
Map, converted at the boundary viaObject.fromEntries/spread), not byhasOwnguards — that is the boundary with design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792. (byService()'s crash is a??=read, but its fix is container construction, so it lives here; design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792's Phase 3 audit should disposition it as covered by this design.) - One probe matrix everywhere:
__proto__with a string value and an object value (the setter treats them differently),constructor,toString, an ordinary key — asserting own-property presence, JSON round-trip preservation, and decision 2.
Phased breakdown
- Phase 1 = aggregator(snapshots): SnapshotStore groups by serviceId into plain objects — a '__proto__'/'constructor'/'toString' serviceId crashes byService (blanking all snapshot history) and pollutes Object.prototype during compact #2941 (aggregator snapshots):
Mapgrouping inbyService()+compactInner(), converted at the boundary. Lands first — the only instance that corrupts global state and blanks a whole surface, and its store may already hold a poisoned line. - Phase 2 = record(loose): a '__proto__' field in loose text is silently dropped — parseLoose assigns into a plain object, so the field never reaches the JSONL line (the JSON path preserves it) #2828 + aggregator(cost-explorer): a setup named '__proto__' is silently never saved — save/import assign into a plain object, so the name becomes the store's prototype instead of an entry #2851 (silent loss at the edges): define-semantics construction in
record.mjs's loose parser and in the cost-explorer save/import paths. Independent of Phase 1. - Phase 3 = new issue (filed with this design): closing inventory audit — grep TS/JS/mjs (vendored assets and tests excluded) for the four non-conforming forms with dynamic keys, disposition each (covered / external-key → fixed here or filed as a named follow-up / internal-only → named in the closing evidence), state the contract in the app docs that already state contracts, and close this design with the evidence. Candidates already visible, to be dispositioned not pre-judged: antigravity-direct's schema walkers (
merged[k],sub[name],props[name]— external tool-schema keys); provider-catalog-sync'sreport.providers[providerId]and(added[pid] ??= []); pkgs/id/webObject.assign(records, frame.records)(world.ts:181, a peer-supplied frame); sync-models'prov.models[baseId]family. Gated on Phases 1–2.
Phases 1–2 adopt the origin issues (no parallels — they carry explicit acceptance with verified probes and queued triage packets). Related, deliberately NOT adopted: today's read-side filings #2920 / #2919 (id/web) belong to #2792's Phase 3 audit — their fix is a read guard.
Open questions (stay in the design doc, not filed)
- Lint enforcement of define-semantics (false-positive rate known only after Phase 3's inventory — same deferred question as design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792's for reads).
- Recursive/deep-merge helpers, if Phase 3's audit finds one in-tree (none known).
- Cost-explorer storage-format change (per-name keys) — unnecessary once names are safe; revisit only if that format is revised for another reason.
Filed by the repo design architect (pass 24, 2026-10-31) for developing-today/code. Implementation phases are the linked sub-issues; phases with open questions stayed in the design doc.
Acceptance criteria
- The design doc (
files/designs/code-proto-write-side.md, goal workspace) is adopted or amended with the decision recorded on this issue. - Each filed write-side instance (the three surfaces named in the design, incl. aggregator(cost-explorer): a setup named '__proto__' is silently never saved — save/import assign into a plain object, so the name becomes the store's prototype instead of an entry #2851) is linked as a phase of this epic, with its fix approach stated in define-semantics terms.
- The closing inventory audit is posted: every externally-keyed write site in the prototype family is listed and dispositioned (define semantics, guarded, or filed as a follow-up).
- Read-side counterpart design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792 is cross-linked and the boundary between the two classes stays explicit.
Activity
dezren39 commentedon Oct 31, 2026
agent-note:v1 — design-architect pass 24 (2026-10-31): filed this design this pass. It is the write-side counterpart of #2792, whose doc scoped this class out to a future design; pass 23's recorded trigger (a second write-side instance after #2851) is met by #2828 + #2941. Phases adopted as sub-issues (verified in this issue's sub_issues list: 2941, 2851, 2828, 2954): Phase 1 = #2941, Phase 2 = #2828 + #2851, Phase 3 = #2954 (new, gated on Phases 1–2). Decisions recorded in the body: preserve-don't-reject; prototype-must-not-move as an asserted test property; accumulators fixed by container construction (Map), not hasOwn. Design doc: files/designs/code-proto-write-side.md in the keep-the-four-repos-healthy goal workspace; register entry added (status: proposed). No user/review — the origins carry explicit acceptance and the rule generalises in-tree idioms (the aggregator's own Map groupings; record's JSON path).
dezren39 commentedon Oct 31, 2026
Intake review
files/designs/code-proto-write-side.md). Still live — no merged PR closes the class; the instances remain the evidence.dezren39 commentedon Nov 1, 2026
Agent note (program tending, 2026-11-01): child progress — sub-issue #2851 CLOSED 2026-11-01T10:28:28Z (parent link REST-verified this run). This epic's last comment (2026-10-31T06:25Z) predates the closure, so the landing is recorded here. Progress note only — not a completion proposal.
dezren39 commentedon Nov 2, 2026
Program tending — sub-issue progress (2026-11-02)
Closed since this thread's last activity: #2828 (closed 2026-11-02T07:02Z).
Recorded for the parent rollup; this is a progress note, not a completion proposal — the parent stays open on its remaining children.
Merge pull request #4956 from developing-today/queue/7683-2954-define…
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: done — design #2953 closed by its Phase 3 audit
task 7683 (lane code-j), Phase 3 = #2954. All three phases have landed:
Contract statement (rule + probe matrix) is now in
pm-stack/aggregator/README.mdand thebin/record.mjsheader.Remaining follow-up, named per this design's own open question (deep-merge helpers: report, don't pre-decide): #4957 —
deepMergeProvidersinbin/provider-catalog-sync.mjs, a recursive assignment-semantics merge over runtime-overlay config keys, with the #2953 probe matrix as its acceptance. It is the only (b) site not fixed inside Phase 3, and it is filed and linked from #2954 — nothing else from the sweep is outstanding.Closing this design issue as complete with #4957 named as its remainder.
developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03