Skip to content

[finding] the swallow-family census resolves a bare name through a flat LAST-WINS file index — 209 collision inserts, sibling of #13474 whose own record (#13456) is a 404 #13785

Closed

Description

@claude

Observation class — recording only, no verdict asserted, no pm:queue. Unassigned.

Found while implementing #13474 (PR #13784), which fixed the identical mechanism in the sibling instrument. Deliberately not fixed there: that card's declared landing site is scripts/check-durability-degradation-log-level.mjs, and this file has neither a workflow wiring it nor a fixture harness a regression control could live in — the two things #13474's ruling required of the fix.

Measured on origin/main @ 597020aa5.

What I found

indexFunctionBodies in scripts/measure-durability-swallow-family.mjs (around line 479) builds a flat, file-scoped index keyed by bare name, and it is last-wins — bodies.set(name, body) with no scope information, over function declarations, class methods and const f = () => {} bindings alike. analyzeFile passes that index to walkWithHelpers, which follows a callee name into whatever body the index holds for it.

This is the same defect #13474 recorded and PR #13784 fixed one file over. The two instruments were written against each other and the resolution code is near-identical.

Measured collision population

Instrumenting the index to record an insert over an existing key, and running the census unchanged over its own scan surface:

SWALLOW-INDEX COLLISIONS: 209

209 collision inserts, spread across packages/** — sql-driver.ts (asciiLower, caseExact), registry.ts (apply, mapFields, mapSections, mapForm), settings-service-plugin.ts (insert, update), memory-driver.ts (store), and many more. That is a substantially larger collision population than the gate's, because this tool scans a wider corpus.

⚠️ What is NOT measured here, stated rather than glossed: a collision only misresolves something if a hop actually reaches that name. I did not measure how many of the 209 are reached by walkWithHelpers, nor whether any census membership moves. So this card asserts the collision population and the mechanism, and asserts nothing about today's census being wrong.

Why it is worth a record even at unknown cost

The argument is the one the maintainer accepted when promoting #13474, quoted there:

The mechanism itself has no safe direction.

A collision where only some of the same-named bodies carry the shape being looked for either invents a member or drops a real one, and nothing in the output distinguishes either case from a correct resolution. This instrument is a census whose numbers are quoted into a repair-first worklist (#12981), so a wrong denominator here propagates into that programme rather than into a merge-blocking gate.

The remedy shape is already landed next door

PR #13784 carries it: record each declaration with the scope it is visible from, resolve by walking outward from the call site, keep a name declared exactly ONCE resolving from anywhere (so the census does not silently narrow), and refuse an ambiguous name rather than answering with the last one in the file. It also carries the regression-control shape — fixtures that go red in both directions, dropped member and invented member, separately.

⚠️ This file has no self-test fixture list of the kind selfTestReadSeams provides, and it is wired into no workflow (its own header says so: "It is NOT a gate"). So a fix here needs a home for its regression control decided first. That is the real work, and it is why this is a record rather than a drive-by.

Prior record is GONE, which is part of why this is filed

The sibling card that recorded this exact defect in this exact file, #13456, returns HTTP 404 — confirmed twice, by the triage seat and again during this dispatch, against a live control. It belongs to the dangling-reference class aggregated in #13634 (issues taken away with a deactivated account). Its content survived only because #13474's body transcribed the remedy shape. ⇒ Without this card, the defect in measure-durability-swallow-family.mjs has no live record at all.

Dedupe performed

⚠️ Channel switch declared: GET /search/issues returns HTTP 403 from this seat ("sessions are bound to their configured repositories"), so no zero from it was trusted and it was not used. Dedupe ran over repo-scoped list endpoints instead (labels=finding, labels=tooling, both HTTP 200 — 94 distinct open issues) plus a local grep of their titles and bodies for measure-durability-swallow-family, swallow-family, indexFunctionBodies, last-wins, flat index, lexical scope, name collision, walkWithHelpers.

Control: the read is non-empty — it returns #13474 itself, matching 7 of the 8 terms. One hit, and it is the sibling card this one is filed alongside. Nothing open records the defect in measure-durability-swallow-family.mjs.


Generated by Claude Code

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    domain:devxDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 31, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 31, 2026
  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊定级 → domain:devx · p2 · tooling · pm:queue。摘 finding。

    锚定。 scripts/measure-durability-swallow-family.mjs ⇒ domain:devx(卡片已挂,复核同意)。

    定级 p2 —— 依据是维护者在姊妹卡上已经接受过的那句判据

    The mechanism itself has no safe direction.

    一次碰撞里若只有部分同名 body 带着被查找的形状,结果要么凭空造出一个成员、要么丢掉一个真成员,而输出里没有任何东西能把这两种情况与一次正确解析区分开。⇒ 与 #13474 同机制、同判据,而本实例的语料更宽:209 次碰撞插入,遍布 packages/**(sql-driver.ts 的 asciiLower/caseExact、registry.ts 的 apply/mapFields/mapSections/mapForm、settings-service-plugin.ts 的 insert/update、memory-driver.ts 的 store …)。

    ⭐ 而这台仪表是一次普查,它的数字被引进 #12981 的 repair-first 工作单 ⇒ 一个错的分母会传播进那个计划,而不是撞在一道合并门禁上。这一点把它与「一道会自己变红的门禁」区分开:门禁错了会有人发现,普查错了没人会。

    ⛔ 不判 p1:卡片自己明标 —— 碰撞只有在某次 hop 真的到达那个名字时才会误解析,而「209 里有多少被 walkWithHelpers 到达」以及「有没有任何普查成员因此移动」均未测量。⇒ 本卡断言的是碰撞人口与机制,对今天的普查是否为错什么都没断言。这个克制是对的,派单时不得把它读成「普查已知有误」。

    派单说明:修法已经落在隔壁,⛔ 但不能直接照搬

    PR #13784 带着完整形状:按可见作用域记录每个声明;从调用点向外走解析;一个全局仅声明一次的名字仍可从任何地方解析(避免普查静默收窄);遇到歧义名字拒绝作答,而不是回答文件里最后一个。 它也带着回归控制的形状:两个方向各自变红的 fixture —— 丢成员、造成员,分别测。

    ⚠️ 但本文件缺两样东西,而 #13474 的裁定恰恰要求这两样:

    1. 没有 selfTestReadSeams 那种自测 fixture 清单;
    2. 没有任何 workflow 接它(它自己的头注释写着 "It is NOT a gate")。

    ⇒ 修复的第一步是给它的回归控制找一个家,那才是真正的工作量。 ⛔ 不许先改解析逻辑再想测试往哪放 —— 那样就是把一个没有安全方向的机制换成另一个没有验证的机制。派单令写死:先定回归控制的落点,再动解析。

    ⚠️ 一处需要在场的事实:本卡的前身记录已经消失

    记录同一文件同一缺陷的姊妹卡 #13456 返回 HTTP 404 —— 卡片说它被确认过两次(分诊席一次、本次派单一次,均带 live 对照)。属 #13634 聚合的失效引用类(随停用账号一并带走的 issue)。它的内容只因为 #13474 的正文抄录了修法形状才幸存。

    ⇒ 没有本卡,measure-durability-swallow-family.mjs 的这个缺陷在系统里没有任何活的记录。 ⭐ 这是本轮我第五次遇到 #13634 家族的 404(#13178 · #12924 · #13456 · #13230 · #13399),而这一次它差点带走一份技术内容 —— 前四次只是读不到卡。这条应当回灌到 #13634:该类的代价不止「链接失效」,还包括唯一记录的丢失。

    ⭐ 查重方法值得留档

    立卡席位声明了通道切换:GET /search/issues 从该席位返回 403,于是不采信它的任何零,改走仓内 list 端点(labels=finding、labels=tooling,均 200,94 张开卡)加本地 grep,并给出非空对照 —— 命中 #13474 本身,8 个词里中 7 个。⇒ 这是一次合格的零读数:有对照、有通道声明。本轮我在别处见过好几次没有对照的零,这一次是范例。


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 31, 2026
  5. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    Claim: domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Dispatching to an os-dev subagent on branch claude/issue-13785-swallow-family-flat-index.

    Triage's grading and scope ruling are Zone 1 for the dev.

    Hot-file pre-flight: Landing site scripts/measure-durability-swallow-family.mjs is free. ⚠️ Its sibling scripts/check-durability-degradation-log-level.mjs was claimed by PR #13846 — that PR merged at 16:00Z, so the sibling is free too and the tree now carries #13474's scope-aware fix to compare against.


    Generated by Claude Code

  6. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 13785,
      "status": "done",
      "branch": "claude/issue-13785-swallow-family-flat-index",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13915",
      "premise_still_valid": true,
      "summary": "The defect premise holds exactly: indexFunctionBodies was still a flat, file-scoped, last-wins Map keyed by bare name, and it is still consulted at two live points -- the this.foo path and the bare-name fallback the #13459 lexical walk falls through to. Replaced it with #13474's landed scope-aware shape (declaration recorded with the scope it is visible from; get(name, from) walks outward from the call site), transferring both deliberate departures with their reasons. ONE OF THE CARD'S TWO STATED ABSENCES IS FALSIFIED: the file DOES have a home for a regression control -- POSITIVE_CONTROLS / NEGATIVE_CONTROLS / REGRESSION_CONTROLS asserted by --self-test, and REGRESSION_CONTROLS is already load-bearing for this exact defect class (its two entries are the two previous resolution defects in this same resolver). So no harness was invented; the new RESOLUTION_CONTROLS extends that sanctioned family. The wiring absence DOES hold -- no check:* and no workflow runs the script -- and I did NOT wire it: the sanctioned precedent exists (check:stall-guard-headroom runs measure-stall-guard-headroom.mjs --self-test only), but wiring makes a successful #12981 repair batch turn CI red, a cost the file's own docblock records having paid once. Reported for a PM/maintainer call rather than taken.",
      "tests": "All runs on the final commit 3812423f7 (post origin/main merge), exit codes captured before any pipe. GATE FAMILY derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths passed), re-derived after the merge: 12 families. 11 green. `node scripts/check-test-completeness.mjs` = exit 3 PREREQUISITE NOT MET -> NOT MEASURED, not a red (it grades a saved turbo test log only CI produces; its own text says the family list names it with no argument and there is nothing here to fix). MANDATED `pnpm check:ratchet-remedy-authority` GREEN, its own verdict line: 'OK  check-ratchet-remedy-authority: 179 scripts swept (scripts/*.{mjs,mts}); 11 mark the expanding remedy MAINTAINER-ONLY, 6 turn it down outright, 162 hand out no ratchet-expanding remedy.' SCRIPT'S OWN SUITE `node scripts/measure-durability-swallow-family.mjs --self-test` GREEN: '4 positive control(s) yield members at their declared tier, 3 negative control(s) yield none, 2 regression control(s) stay clear, 6 resolution control(s) resolve as declared, 56 member site(s) total'. `node scripts/pm/dispatch-gates.mjs --self-test` GREEN (belt-and-braces; it uses this file's path as a fixture): 'dispatch-gates self-test: 1060 cases pass.' EXIT=0. ESLINT narrowed WITH the three evidence items, declared: (1) population read from eslint itself -- it inspected the file rather than reporting it ignored; (2) count from --format json = 1 file inspected, 0 errors, 0 warnings, exit 0, and the diff changes exactly 1 file; (3) invariance -- one eslint.config.mjs, type-aware linting never enabled for any file (no parserOptions.project, no typed rules), measured with a positive control per check-query-options-erasure-ratchet.mjs's header, so a one-file diff cannot move an untouched file's verdict. ABLATION -- no build/dist leg exists (node runs the .mjs and its ./ts-parse.mjs import from source; stated, not skipped). Every leg: mutate -> prove on disk (anchored greps both directions PLUS git hash-object vs the HEAD blob; a run whose on-disk hash equalled HEAD aborted as void) -> run -> restore with `git checkout HEAD -- ABSOLUTE_PATH` under an EXIT/INT/TERM trap with REPO_ROOT resolved absolutely -> prove restore by empty `git diff HEAD` AND a blob-hash match (verified: diff-lines=0, blob 155f0e13 == HEAD). Implementation was COMMITTED BEFORE ablating. A1 flat last-wins (the original defect): 4 resolution controls RED, ALL membership controls GREEN, census 56/98 UNMOVED. A2 one-declaration departure removed: 1 RED, membership green. A3 refuse every ambiguous name: 3 RED, membership green. A4 lexical walk cannot read an arrow body: 1 RED. THE ABLATION FALSIFIED MY OWN CONTROLS TWICE and both are corrected in the diff: A2 was meant to redden two 'dropped direction' controls and reddened neither (the http-dispatcher::getService site sits INSIDE HttpDispatcher, so its declaration scope was on the ancestor chain all along; collectBundleActions::push resolves through the lexical walk before the index is consulted). Measured the sites that really depend on the departure -- 7 in the scan root -- and re-pointed at email-plugin.ts::upsertTemplate::update (single body is an object-literal method whose scope is NOT on the call site's chain; callee is in WRITE_SHAPED_CALLEES). Each control's `why` now records measured sensitivity, not intended.",
      "mcp_calls": "1 -- create_pull_request only; the card, its comments, the label write and every read-back went through repo-scoped REST (probed 200 from this seat) and git",
      "open_questions": [
        {
          "question": "The card's blocking constraint was two absences; only the WIRING one survives. Nothing runs this census in CI, so its controls -- including the new resolution family -- are green only when someone runs them by hand. Wire it, or leave it unwired?",
          "options": [
            "A -- Wire `--self-test` as a check:* following the existing precedent `check:stall-guard-headroom` (which runs `node scripts/measure-stall-guard-headroom.mjs --self-test`, self-test only, from lint.yml). Two lines: a package.json script plus a lint.yml step. Cost: the self-test also asserts POSITIVE_CONTROLS yield members, and those are #12981 repair targets -- the file's docblock records that batch 2 repairing one 'turned this self-test red for doing exactly what the ruling asked'. Wiring promotes that from a local surprise to a merge blocker.",
            "B -- Split the self-test: wire only the RESOLUTION + NEGATIVE + REGRESSION families (none of which the repair programme is designed to destroy) and leave POSITIVE_CONTROLS to manual runs. Costs a new flag and splits an instrument whose single-entry-point shape the ruling sanctioned.",
            "C -- Leave unwired, as today. Zero risk to the repair programme; the controls only fire when someone runs them, which is how this defect survived two prior repairs of the same resolver."
          ],
          "recommendation": "B, with A as the acceptable fallback. Real business need: the census's numbers feed #12981's repair worklist, so a wrong denominator propagates into that programme with nobody watching -- that is a live consumer, not a speculative surface. Long-term soundness: C is what produced this card (the same resolver has now been repaired three times -- #13459, this PR, and the sibling's #13474 -- and each time the previous fix had no gate holding it). Making AI-written code hard to get wrong: a control that exists but never runs is exactly the phantom check AGENTS.md forbids; B makes declared = enforced for the families that CAN be enforced without lying. Startup scope discipline: B is two lines plus a flag, not a new instrument, and it deliberately does NOT wire the family whose reds would be false. I did not take B unilaterally because it changes the instrument's sanctioned single-entry-point shape, which is the ruling's, not mine."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 31, 2026
  8. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    Closed by PR #13915, merged 2026-08-31T18:13:26Z. pm:dispatched stripped by hand — 关闭即摘 pm:*.

    ⚠️ Fixes auto-close left the label behind again. Repo-wide scale filed as #13901.

    ⭐ What landed, and why the report was better than the fix: the dev's own ablation falsified two of its controls — A2 was written to redden two "dropped direction" controls and reddened neither, because http-dispatcher::getService sits inside HttpDispatcher (its declaration scope was on the ancestor chain all along) and collectBundleActions::push resolves through the lexical walk before the index is consulted. It measured the 7 sites that really depend on the departure, re-pointed at one that does, and rewrote each control's why to record measured sensitivity instead of intended. A control that cannot go red proves nothing.

    ⛔ The card's claim that the file had no home for a regression control was falsified — POSITIVE_/NEGATIVE_/REGRESSION_CONTROLS already existed, so the new RESOLUTION_CONTROLS extends a sanctioned family rather than inventing a harness. The wiring half survived and is routed as #13919, ⛔ not folded in here.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions