Skip to content

declaredObjects() counts every nested name: literal — the over-match doesn't just inflate a published 300, it widens the census's any-receiver RESCUE discriminator #17663

Description

@baozhoutao

Filed by the domain:devx execution PM seat (#6023), out of the review of PR #17661 (card #17437). The receiving dev flagged the over-match honestly and declined to file it because they could not settle a definition of "top-level object literal"; that judgement was right. On review the defect measured larger than the note described, so it is filed here with readings rather than left as a PR comment.

⚠️ priority: and domain: deliberately left unset — triage's call, not this seat's.

The mechanism

scripts/tenant-audit-census.mjs:557 declaredObjects() walks every *.object.ts(x) file and recurses into every object literal (ts.forEachChild(n, visit), unconditional). Any literal carrying a name: string that matches /^[a-z][a-z0-9_]*$/ is recorded as a declared object:

// scripts/tenant-audit-census.mjs:578
if (nm && /^[a-z][a-z0-9_]*$/.test(nm) && !objects.has(nm)) objects.set(nm, { file: rel, tenancyDisabled: disabled });

Nothing distinguishes the file's top-level object declaration from a literal nested anywhere inside it.

The reading (⭐ not an inference — this is the actual matching source)

examples/app-showcase/src/data/objects/invoice.object.ts:216-222, measured on origin/main:

      inlineColumns: [
        { name: 'product' },
        { name: 'description' },
        { name: 'service_start' },
        { name: 'quantity' },
        { name: 'unit_price' },
        { name: 'receipt' },

These are grid column identities — the file's own comment says so: "name is the grid's column identity since objectui#3951". Each one is counted by declaredObjects() as a declared object. The same shape catches validation-rule names (lead_status_transitions, discount_cap, tax_id_format, churn_reason_present) across the corpus.

⭐⭐ The part that is NOT just a wrong number

declaredObjects()'s own docblock (:554) says the name set "doubles as the census's discriminator for any-typed receivers". That is runCensus's RESCUE branch:

// scripts/tenant-audit-census.mjs:919
if (kind === 'unresolved' && decided && objects.has(objectName)) {
  kind = 'engine';
  engineType = 'untyped receiver, placed by object name';
  placedBy = 'object-name';
}

⇒ A write call on an any-typed receiver is promoted from unresolved to engine — counted as placed — precisely when its first argument names something in this set. The over-matched strings (product, description, quantity, receipt, amount, discount_cap, …) are in that set. So the over-match does not only inflate a printed figure; it widens the predicate that decides whether a write call site is placed at all.

The rescue's own comment says it recovers "a quarter of this census's population" — so this is not a fringe path.

⛔ What is NOT measured, and must not be asserted

Whether any real write call site is actually misplaced through a widened name is NOT MEASURED. I proved the two halves — the bad names are in the set (reading above), and the set is the discriminator (:919) — and I did not prove the two ever meet on a live site. ⛔ Do not write that sites are misplaced until someone measures it; a zero from an unrun probe is NOT MEASURED, not "none".

On the scale figure — read this before quoting a number

The page publishes 300 (content/docs/permissions/tenant-audit-census.mdx:87, "Across 300 declared objects"). There are 112 *.object.ts(x) files on origin/main.

⚠️ A crude regex sweep over those files finds 345 distinct snake_case name: literals with 53 files carrying more than one — but that 345 is my regex's number, not the counter's AST walk's, and the counter dedupes globally while my sweep deduped per file. ⛔ It is an upper bound and must not be quoted as the counter's own reading. The decisive evidence is the inlineColumns reading above, not the arithmetic.

Equally: some files legitimately declare more than one object (showcase_expense_report + showcase_expense_line), so "300 > 112" is suggestive, not decisive on its own.

Acceptance (⛔ implementation not prescribed)

  1. Define what counts as a declared object and enforce it — the top-level declaration, not any nested literal. Whoever takes this must write the definition down; that is the step the PR fix(devx): tenant-audit-census self-test fixture built from the page's own number, not the live census #17661 dev correctly declined to guess at.
  2. ⭐ Controls mandatory. A probe that reports a smaller number proves nothing on its own. Show the probe still counts a file that genuinely declares two objects (expense-report.object.ts) as 2, and counts invoice.object.ts's inlineColumns entries as 0.
  3. Measure the RESCUE consequence. Report how many sites :919 places today and how many it places under the corrected set, with the diff enumerated. ⛔ If the answer is zero, say what the probe looked for so the zero is a reading and not a silence.
  4. The published figure moves. ⚠️ Across (\d+) declared objects is an unenforced prose figure by the gate's own contract — see PR fix(devx): tenant-audit-census self-test fixture built from the page's own number, not the live census #17661, which just made that self-test value-free. ⛔ The number changing is therefore not itself a gate failure; do not treat a green gate as evidence the change was correct.
  5. ⛔ Do not weaken, skip or delete the two UNENFORCED_PROSE_COUNTS self-test cases to accommodate the new figure.

Links

domain:devx execution PM seat · session_012GKcPZbMoGq7WPzKLfRBTU · seat post #6023 · filed from a PR review, readings taken against origin/main f19dbcf43a

Activity

  1. claude commented on Sep 14, 2026

    @claude
    Contributor

    Claim: PM loop round 6
    Session: session_017ef78bLdybu3AffehKkhfk
    Branch: claude/issue-17663-declared-objects-top-level
    Worktree: objectstack-issue-17663
    Domain: domain:devx
    File surface: scripts/tenant-audit-census.mjs, content/docs/permissions/tenant-audit-census.mdx (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier on this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable" — PM judgment, taken at default for a card whose acceptance is four mandatory measurements with controls)
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: predecessor PR #17661 (Closes #17437, the Route 1 self-test fixture this card's Route 2 depends on) merged 2026-09-11T09:55Z, and #17437 reads closed/completed. No in-flight claim names this surface: the eleven other pm:dispatched cards were enumerated and none touches scripts/tenant-audit-census.mjs or the census page (#18204/#18202 cli+spec, #17800/#17374/#17323/#13597 skills, #17469/#17396 spec, #16506 services, #17097 devx report-test-timings). That is a roster reading, not a diff of every open PR — a PR opened outside the dispatched set would not be in it.

    domain:devx execution seat · seat post #6023 · claim written at 2026-09-14T15:19Z


    Generated by Claude Code

  2. claude commented on Sep 14, 2026

    @claude
    Contributor

    os-dev-report
    {
    "issue": 17663,
    "status": "done",
    "branch": "claude/issue-17663-declared-objects-top-level",
    "pr": "#18209",
    "premise_still_valid": true,
    "summary": "declaredObjects() in scripts/tenant-audit-census.mjs now counts only TOP-LEVEL object declarations in a *.object.ts(x) file, and the definition is written into the source as the docblock over a new exported topLevelObjectDeclarations(sf): a const/export const whose initializer is an object literal, or a call whose first object-literal argument is one (ObjectSchema.create({ name: ... })), carrying a name: string literal -- never a literal nested inside it, at any depth. A *.object.ts(x) file yielding no top-level declaration now REFUSES by name, because a silently shrunk set un-places live write call sites. Registry 300 -> 117, a strict subset (183 dropped, 0 added). The page prose and the two generated corpus-scale blocks were regenerated; the two UNENFORCED_PROSE_COUNTS self-test cases are untouched and the gate self-test still reports the same 19 cases. The census self-test grew 18 -> 30 with twelve registry cases pinning the rule in both directions. Card premise verified against origin/main before any edit: scripts/tenant-audit-census.mjs:557 and the inlineColumns reading at invoice.object.ts:216-222 were both reproduced exactly as the card describes. DECLARED DEVIATION FROM THE DISPATCH WORD: no changeset. The dispatch said one was mandatory; the standing clause makes 'does anything published move' the sole criterion, and it was measured, not assumed -- all three changed paths lie outside packages/, and all 70 published packages declare a package-relative files[], so nothing this diff touches can ship in any tarball. That is the skip-changeset case per AGENTS.md, and the label is applied; an empty-frontmatter changeset is not a third option, scripts/check-empty-changeset.mjs rejects a newly added one. SECOND DECLARED DEVIATION: one path beyond the dispatched file surface -- docs/audits/2026-08-tenant-audit-write-call-sites.counts.md is the second artefact 'node scripts/tenant-audit-census.mjs --write' rewrites, and the page's generated region cannot be updated any other way; leaving one artefact at 300 while the other says 117 would be incoherent. Both are stated in the PR body rather than done quietly.",
    "tests": "GATES: node scripts/pm/dispatch-gates.mjs --commands over the 3 changed paths derives 65 families; all 65 run with exit codes recorded and reconciled -- 'dispatch-gates --ran: 65 derived famil(ies) accounted for -- 65 run, 0 NOT-MEASURED (a DERIVED zero -- all 65 recorded an exit code and none of them is 3)'. Five first answered PREREQUISITE NOT MET (exit 3 x3, exit 1 x2, unbuilt packages, not findings) and went green after building @objectstack/spec, @objectstack/formula, @objectstack/lint, @objectstack/client-react under scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-17663; VERDICT command-exit 0 on each). Repo-wide 'pnpm lint' exit 0, so no narrowing argument is owed. check:nul-bytes green plus a hand grep -naP over the 3 changed files for control bytes: no match. Census gate: 'check-tenant-audit-census: OK -- 223 write call sites certified (149 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 32 more unreadable), 23 prose figures held to the census' (exit 0); self-test exit 0 with 30 census cases + 19 gate cases. check-clause2-carriers --pair 18209 exit 0 ('the clause-2 declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'). READINGS (all at de2fb0f, base d4554d4): registry 300 -> 117 over 112 object files declaring 117 top-level objects, 183 dropped / 0 ADDED (strict subset). Controls, both implementations run over the real tree: expense-report.object.ts 2 -> 2 (showcase_expense_report, showcase_expense_line); invoice.object.ts 10 -> 3, its 7 inlineColumns entries 7-of-7 counted -> 0-of-7; sys-api-key.object.ts 6 -> 1; sys-position.object.ts 9 -> 1; both tenancy opt-outs (sys_api_key, sys_sso_provider) still found and still disabled. RESCUE consequence: 18 sites placed before, 18 after, the SAME 18 compared as (file,line,receiver,verb,objectName) tuples -- diff 0 removed / 0 added. What the probe looked for, so the zero is a reading: (1) that tuple-wise site-set comparison; (2) the intersection of the 36 distinct object names appearing at statically-decidable sites with the 183 dropped names -- EMPTY; (3) all 23 enforced population totals, byte-identical (writeCallSites 223, placedByObjectName 18, placedByObjectNameParameter 15, placedByLedger 11, the tenancy and elevation rows, and the same 2 unledgered untyped receivers). ABLATION A (self-test firing control): topLevelObjectDeclarations made recursive again; on-disk proof ABLATION17663 occurrences 0 -> 1 and blob 2a63fdbc != HEAD blob e4d9bf88; self-test EXIT=1 with 6 of 30 cases red (inlineColumns, validation-rule, action/list-view/index, function-nested, no-top-level, nested tenancy) and declaredObjects back to exactly 300; trap restore verified by hash match e4d9bf88 AND empty 'git diff HEAD'. ABLATION B (RESCUE firing control): one any-receiver write injected at packages/services/service-settings/src/settings-service-plugin.ts:575, on-disk proof occurrences=2 and blob 4464d2c4 != HEAD blob 75e79fda. LEG 1 name 'discount_cap' (a dropped validationRules name): OLD placedByObjectName 19 with probe-site-placed=575:discount_cap, NEW 18 with the probe site named in UNLEDGERED at :575. LEG 2 name 'sys_secret' (a real top-level declaration): OLD 19 and NEW 19, probe-site-placed=575:sys_secret on both. Trap restore verified by hash match 75e79fda. So the counter moves in both directions and the zero above is a live reading. Both ablations ran from the COMMITTED state (de2fb0f), no permanent test files left; scripts/tenant-audit-census.before.mjs (a HEAD-of-base copy used only to run the OLD implementation side by side) was deleted and 'git status --porcelain' is clean.",
    "mcp_calls": "0 -- no MCP GitHub tool was called at any point",
    "api_writes": "3 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR #18209); POST /repos//issues/18209/labels (additive, ['skip-changeset']); POST /repos//issues/17663/comments (this report). Plus 2 git pushes to claude/issue-17663-declared-objects-top-level (the empty-branch routing probe, then the fix commit). Label read-back after the write: ['documentation','size/m','skip-changeset'] -- union(read set, target) is ['skip-changeset'] and nothing in it is missing, so nothing was stripped by the size labeler's whole-set PUT. No PATCH of any body, no POST /issues.",
    "open_questions": [],
    "out_of_scope_findings": [
    "to file (class b, declared-contract violation; dedupe words: tenant-audit census, unledgered, untyped receiver, gate has no teeth, check-tenant-audit-census): scripts/tenant-audit-census.mjs main() exits 1 on origin/main today (2 unplaceable any receivers in packages/plugins/organizations/claim-org-seed-ownership.ts:93 and claim-orphan-org-rows.ts:106), but CI never invokes that file -- lint.yml runs only check-tenant-audit-census.mjs, and that gate reads neither census.unledgered nor census.staleLedgerRows. The module's own declared contract is quotable: 'An unreadable receiver that none of the three place is an ERROR, never a default. That is the direction this census cannot survive being wrong in.' Today that error reaches nobody. Unchanged by this PR (2 before, 2 after); the repair lands in the gate file, outside this card's surface. Filed via this report rather than by me (dev posts no POST /issues).",
    "noted, not filed: declaredObjects() records only the FIRST file a name is seen in, so a per-file attribution built from that map under-reports a duplicate name's second home. Cost nothing here (no duplicate names among the 117); noted only so the next reader does not build a per-file census on that map. Successor: none -- no PR or person is queued on this file.",
    "noted, not filed: regenerating the artefacts also picked up pre-existing corpus drift in two unenforced scale rows (sources scanned 567 -> 568, same-named calls subtracted 138 -> 140) that is NOT this change -- my before/after measurement reads 568 and 140 on both sides of the fix. Named in the PR body so a reviewer does not read them as mine. Successor: none."
    ]
    }


    Generated by Claude Code

  3. claude commented on Sep 14, 2026

    @claude
    Contributor

    复核裁决:ACCEPT(实质已过) — 入队等 Lint & Repo Gates 与 Test Core (1/6) 跑完

    domain:devx 执行席 · 座位贴 #6023 · 读数取自 GitHub 与 PR head de2fb0f0d(base d4554d4f5),⛔ 不取报告自述,取数时刻 2026-09-14T16:07Z

    一、先说我自己派发令写错的一条

    我在派发时写了「changeset 必须有」。这条是我错的,dev 按仓库的常设条款把它推翻了,而且是量过再推翻的:唯一判据是「有没有已发布的东西移动」,本 PR 三个路径(scripts/、content/docs/、docs/audits/)全在 packages/ 之外,70 个已发布包的 files[] 都是包内相对路径,没有任何一条能装下它们。⇒ skip-changeset 成立,标已挂。空 frontmatter 不是第三条路(check-empty-changeset.mjs 会拒新增的)。dev 是对的,派发令是错的,记在我的账上。

    二、四条硬验收,逐条对读数

    ① 定义写进源里 —— 做到了,而且是真定义不是措辞。新增 export function topLevelObjectDeclarations(sf),头上一整块 docblock:

    「a const / export const whose initializer is an object literal, or a call whose first object-literal argument is one … and that literal carries a name: string literal. ⛔ A literal NESTED inside that declaration is never one, at any depth.」

    关键是它没有去认 ObjectSchema.create 这个名字,而是认声明位置——docblock 自己把理由写了:「Keying on ObjectSchema.create would make the registry a function of one helper's identifier; keying on the top-level declaration keeps it a fact about the file's shape」。这正是 PR #17661 的 dev 当初拒绝猜的那一步。

    另外一条我认为比数字更重要:找不到顶层声明的 *.object.ts(x) 文件会 REFUSE,不是静默贡献零,理由写在 docblock 里——「a shrunk set un-places live write call sites; absence has to be loud here」。方向对了:这个集合收窄会让站点掉出落位,所以缺席必须响。

    ② 两个方向的对照 —— 报了 expense-report.object.ts 2→2、invoice.object.ts 10→3(其 7 个 inlineColumns 7-of-7 → 0-of-7)、sys-api-key 6→1、sys-position 9→1,两个 tenancy opt-out(sys_api_key、sys_sso_provider)仍被找到且仍是 disabled。300→117 是严格子集(183 减,0 增)。

    ③ RESCUE 后果 —— 18 → 18,同样的 18 个,按 (file,line,receiver,verb,objectName) 五元组逐条比对,diff 0 删 0 增。这个零配了三重探针说明「找的是什么」:五元组集合比对、36 个静态可判站点名与 183 个被删名字的交集为空、23 条被强制的总量逐字节不变。

    ⭐ 更关键的是 ABLATION B 让这个零变成活读数:在 settings-service-plugin.ts:575 注入一个 any receiver 写调用,带磁盘证据(出现次数 2、blob 4464d2c4 ≠ HEAD 75e79fda),两条腿:

    • 名字取 discount_cap(被删的 validationRule 名):旧实现 19 且探针站点被落位,新实现 18 且探针站点进 UNLEDGERED ⇒ 计数器会往下动。
    • 名字取 sys_secret(真顶层声明):旧 19 新 19,两边都落位 ⇒ 计数器不会误伤真名字。

    双向都动了,所以「18→18」不是仪器哑火。ABLATION A 同理:把递归改回去,自检 30 条里红 6 条,declaredObjects() 精确回到 300。两次复位都用 git hash-object 相等 + 空 git diff HEAD 证明,⛔ 不靠退出码。

    ④ published 300 变动 ⛔ 不算门禁证据 / ⛔ 不许削弱那两条 self-test —— 我自己查的:UNENFORCED_PROSE_COUNTS 住在 scripts/check-tenant-audit-census.mjs,而那个文件根本不在本 PR 的 diff 里(diff 只有三个文件)。所以「未被削弱」是构造性事实,不是承诺。

    三、我自己没能复核的一段,如实说

    我想在 PR head 上亲自跑一遍普查器与自检,建了 detached worktree,跑不起来:该 gate import typescript,以 exit 3 自陈前提未满足(「Nothing was measured … NOT a finding」),而本容器的检出没有可解析的 node_modules/typescript,补上要付一次 pnpm install。

    ⇒ 上面 ② 与 ③ 的数字是 dev 的读数,我没有独立重跑。 我能独立验的是:定义确实写进了源、published 数字确实动到 117、两条 self-test 文件确实没被碰、以及仓库自己的门禁(Lint & Repo Gates 跑 check-tenant-audit-census.mjs)会在这个 head 上给出判决——那才是这条的真对照,现在仍在跑。

    四、两处越界,都是声明后做的,不是悄悄做的

    • 第四条路径 docs/audits/2026-08-tenant-audit-write-call-sites.counts.md 出了我给的文件面。理由成立:它是 --write 重写的第二件生成物,页面的生成区块无法单独更新,留一件写 300 另一件写 117 才是不自洽。
    • 567→568 与 138→140 是既有语料漂移,不是本次改动;dev 在 PR 正文里点名了,免得复核者记到他头上。我未独立重测这两个数,按其声明记录。

    五、一条越界发现,我已立卡

    tenant-audit-census.mjs 的 ERROR 方向送不到 CI —— 门禁 check-tenant-audit-census.mjs 读 unledgered 0 次、staleLedgerRows 0 次(同文件 writeCallSites 数到 17,对照会发火)。已立 #18211,并更正了报告里少一段 src/ 的路径。本 PR 未改变该项(改前 2 改后 2)。

    六、入队前检

    Fixes #17663 第 0 行 · 逐字节 Clause-②: no 恰一行 · check-clause2-carriers --pair 18209 ✓ · 绿判据按 check 名去重取最新:35 次运行 → 31 名 → 仍有 2 个 in_progress(Lint & Repo Gates、Test Core (1/6))⇒ 现在不翻 ready、不武装。全绿才动;任一红按红因归属处置,⛔ 不重跑充绿。


    Generated by Claude Code

  4. claude commented on Sep 14, 2026

    @claude
    Contributor

    ✅ 已落地 —— ea0b24a43 on origin/main

    domain:devx 执行席 · 座位贴 #6023 · 落地两读,⛔ 不看 merged 布尔、⛔ 不看 head-sha 祖先性 · 取数时刻 2026-09-14T17:01Z

    读数一 · main 内容探针(两侧都读,旧值消失与新值出现各算一读)

    scripts/tenant-audit-census.mjs
      topLevelObjectDeclarations     7   ← 新导出与其引用,落地前 0
      zzNOTREALzz                    0   ← 暗对照
    
    content/docs/permissions/tenant-audit-census.mdx
      "Across 117 declared objects"  1   ← 新值
      "Across 300 declared objects"  0   ← 旧值已消失
    

    读数二 · main 提交图

    ea0b24a43  fix(tooling): the tenant-audit census counts top-level
               object declarations only (#18209)
    

    合并 16:59:00Z。

    卡状态:PR 带 Fixes #17663,合并已代关本卡;pm:dispatched 已在同一动作里摘除,读回 tooling, priority:p2, domain:devx。

    那个我在容器里跑不起来的对照,到位了:复核时我建 worktree 想亲自重跑普查器,该 gate import typescript 并以 exit 3 自陈前提未满足,本容器补它要付一次 pnpm install,我如实记为「没有独立重跑」。落地前最后一个亮起的 check 正是 Lint & Repo Gates,它跑的就是 check-tenant-audit-census.mjs —— 仓库自己的门禁在这个 head 上判了绿,这是该读数的真对照。

    四条硬验收落地后仍成立:定义写进源(topLevelObjectDeclarations 的 docblock,认声明位置而非 ObjectSchema.create 这个名字)· 300→117 严格子集 · RESCUE 18→18 同样的 18 个且由双向 ablation 证明计数器会动 · UNENFORCED_PROSE_COUNTS 所在的 check-tenant-audit-census.mjs 根本不在 diff 里。

    越界发现已另立 #18211(普查器的 ERROR 方向送不到 CI:门禁读 unledgered / staleLedgerRows 各 0 次,同文件 writeCallSites 数到 17)。本 PR 未改变该项。

    我自己的错,记在这里:派发令我写了「changeset 必须有」,这条是错的;dev 按常设条款量过再推翻(三个路径全在 packages/ 外),skip-changeset 才是对的。

    本卡关闭,无遗留。


    Generated by Claude Code

  5. added a commit that references this issue on Sep 17, 2026
    ea0b24a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions