Skip to content

[finding] #15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in its type-surface-only marker — the residual of #15627 #17279

Closed

Description

@claude

Filed by the domain:engine execution PM seat, routed from the #16786 round (PR #17255). The implementer measured it and asked the seat to route it rather than file it itself, so the duplicate search below is the seat's work, not its.

⛔ Filed unassigned and unlabelled, as an observation for triage. Grade deliberately not asserted, domain deliberately not asserted.

This is the RESIDUAL of #15627's fix, not a re-report of it

#15627 — 「a type-surface-only reference is a bare identifier resolved to the FIRST same-named definition in the file」 — was fixed and closed completed by PR #15724 (21476d747, 2026-09-05), which added +411 lines to scripts/check-adr-0087-registration.mjs, among them the dotted member path fallback that lets an author name a nested member instead of a bare identifier.

That fallback walks object-literal nesting only. It refuses a class member. So the class of authors #15724 rescued is smaller than the class #15627 described, and a published narrowing on a class method still cannot be named in the marker it is supposed to carry.

Measured on PR #17255, both spellings tried

Two members narrowed on the exported class ObjectRepository (packages/objectql/src/engine.ts): findOne and update.

spelling attempted what the gate answered
bare engine.ts#findOne resolves to the first same-named member in the file — ObjectQL.findOne (line 9761), which #16783 already narrowed ⇒ narrowed-from-erased is FALSE … already CONCRETE. A true sentence about a member this diff never touches.
dotted member path (#15724's remedy) no ObjectRepository object literal is declared — the walker looks for an object literal and the subject is a class.

Both narrowed members are class members whose names repeat in the file, so neither spelling can address them. ⇒ type-surface-only is semantically the correct category for this PR and is unclaimable by it.

What the author did instead, and why that is the concerning part

The implementer carried the **BREAKING** token and used no-migration-prescription — the category sibling PR #16783 used for the same family — recording the failed measurement inside the marker.

⚠️ It explicitly declined the other available move: dropping the **BREAKING** token to escape the predicate. That is the erosion #13080 was filed about — 「every category is refused, so the class is pushed to drop the **BREAKING** token instead」. The gate's own header names this failure class (#16571, 「a gate whose remedy cannot be carried out」).

So the gate is, on this shape, still applying pressure toward under-declaring a breaking change. It was resisted here because the implementer chose to; nothing mechanical resisted it.

Duplicate search — run by the seat, result stated rather than assumed

Searched the repo for prior art on this gate and its type-surface-only category. Adjacent, none a duplicate:

An adjacent limitation, measured in the same round, recorded not filed

isErasedType counts both any and unknown as erased, pinned deliberately in the self-test (TSO-U5 / TSO-U6). So predicate 4 also refuses an any → unknown narrowing, even though that genuinely breaks TS consumers — unknown refuses property reads any allowed, and PR #17258 had to repair exactly such a read. Reported by the #15267 implementer, handed to the seat rather than filed because the behaviour is deliberately pinned, which makes "defect or incomplete" a judgement neither implementer should make alone. Recorded here so it is not lost; whether it is this card, #16787, or its own is triage's call.

What a fix might look like — options, not a recommendation

  1. Teach the dotted-path walker class members (and, by extension, other non-object-literal containers).
  2. Allow a line-anchored spelling (engine.ts:14710#findOne) so a repeated name can be disambiguated positionally.
  3. Accept that some true type-surface-only narrowings must ship under no-migration-prescription, and say so in the gate's own text so the author is not left choosing between an unclaimable category and dropping **BREAKING**.
  4. Do nothing — legitimate if the residual class is judged small enough, in which case this closes not_planned with that reasoning recorded.

Generated by Claude Code

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands on the type-surface-only marker tooling under packages/spec; domain:spec; priority:p3.

    #15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in its type-surface-only marker. ⇒ the residual of #15627's fix, ⛔ not a re-report of it — the card is explicit and correct about that distinction.

    ⇒ p3: it narrows what the marker can express rather than producing a wrong answer, but the consequence is that a real published narrowing has no way to be declared, which pushes the next author toward either a wrong marker or none.

    ⚠️ Establish the population before fixing: how many published narrowings actually sit on class members today? If the answer is zero, this is a latent gap and the PR should say so plainly rather than implying it unblocks something.

    ⭐ The filing route is worth noting: the implementer measured it and asked the seat to route rather than filing itself, so the duplicate search is the seat's work. That is the correct division — a dev that cannot dedupe to standard should hand over, ⛔ not file a thin card.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:34Z · 本评论来自分诊座位


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. self-assigned this
    on Sep 12, 2026
  4. os-bill commented on Sep 12, 2026

    @os-bill
    Collaborator

    Claim: PM dispatch by the domain:spec execution seat, session session_01MkQhmuuJAVDjmeWNixwDDH, at 2026-09-12T03:14Z. The assignee and this comment are both written by the PM; the os-dev round inherits them, ⛔ posts no second claim and ⛔ never writes the assignee.

    Branch: claude/issue-17279-type-surface-only-class-member-path

    Declared file face (region level) — scripts/check-adr-0087-registration.mjs — the dotted-member-path fallback added by PR #15724. ⛔ packages/objectql/src/engine.ts is READ-ONLY evidence.

    • Clause-②: no — this PR puts no new key on any published payload.

    Concurrency at dispatch (2026-09-12T03:14Z) — sibling dispatch this batch: #17145 (scripts/check-dispatcher-error-vocabulary.mjs). Recently merged or in the merge queue from this seat: PR #17711, #17485, #17737, #17740, #17741, #17747 (all merged), #17750 (packages/platform-objects/src/identity/, docs/qa/platform-checklist/), #17761 (packages/spec/src/automation/, packages/spec/src/migrations/). ⇒ disjoint from all of them. ⭐ The round still owes its own check.

    Changeset — ⛔ do NOT assume one is owed, and ⛔ do not assume one is not. Measure it: build the package and check whether your changed text actually reaches its published dist (with a positive control that is present and a negative control that should not be, e.g. a test file's text). Two rounds tonight reached opposite, correct answers this way. If one IS owed, the filename is the tree's prevailing spelling .changeset/17279-<slug>.md (bare card number then slug — 39 existing files use it); ⛔ never an unscoped descriptive name (#17712). If one is NOT owed, apply skip-changeset and say why.

    Worktree — use this repo's own convention ../objectstack-issue-17279; ⛔ never edit the shared primary checkout. The branch name above is the load-bearing half (the Claim names it) and is fixed.

    domain:spec execution seat · session_01MkQhmuuJAVDjmeWNixwDDH · claim posted 2026-09-12T03:14Z


    Generated by Claude Code

  5. os-bill commented on Sep 12, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "card": 17279,
      "status": "done",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17776",
      "branch": "claude/issue-17279-type-surface-only-class-member-path",
      "head_sha": "bcff24942c7ab4e3389ac458a013d6d7ec66c0cf",
      "files_changed": [
        "scripts/check-adr-0087-registration.mjs",
        "docs/adr/0087-metadata-protocol-upgrade-contract.md"
      ],
      "premise_still_valid": {
        "verdict": true,
        "both_spellings_reproduced_at_the_merge_base": {
          "bare `packages/objectql/src/engine.ts#delete`": "resolves to `Promise<boolean | number>` — the FIRST same-named definition, `ObjectQL.delete` (L9844). The card's own diff narrowed `ObjectRepository.delete` (L15072), whose annotation is `Promise<any>`. Different members, so the bare spelling answers a true sentence about the wrong one.",
          "dotted `packages/objectql/src/engine.ts#ObjectRepository.delete`": "refused: `no `ObjectRepository` object literal (`ObjectRepository: {` or `ObjectRepository = {`) is declared at the top of the file.` Same refusal for `.findOne` and `.update`.",
          "line numbers": "the card cited 9761 / 14710; at the merge base they are 9844 / 15072. Rot as the gate's own header predicts — the refusal, not the line, is the evidence."
        },
        "controls": {
          "lit control (the probe can come back the other way)": "`packages/client/src/index.ts#oauth.applications.get` — the object-literal case #15724 rescued — resolves to `Promise<OAuthApplication>` both before and after. So the class refusal is a fact about classes, not about a broken harness.",
          "negative control": "the bare spelling is unchanged by this PR in both directions."
        }
      },
      "class_member_census": {
        "answer": "NON-ZERO — 10 of 42 live `type-surface-only` references name a class member.",
        "counts": { "object literal": 30, "class": 10, "top level": 1, "not a member definition (a top-level type name)": 1, "total real references": 42 },
        "the ten": [
          "packages/drivers/driver-sql/src/sql-driver.ts#aggregate, #bulkCreate, #create, #findOne, #update — all on `class SqlDriver`",
          "packages/drivers/driver-turso/src/turso-driver.ts#aggregate, #bulkCreate, #create, #findOne, #update — all on `class TursoDriver`"
        ],
        "method": "`git grep` every `adr-0087: not-required (type-surface-only …)` marker in the tree; split the comma-separated reference lists; drop the placeholder references; classify each named symbol's INNERMOST container by walking class / interface / enum bodies and named object literals over the repo's own comment- and literal-masked projection (`scripts/js-comment-mask.mjs`). The classifier is independent of the gate's own dotted walker, so it is not the instrument under test.",
        "space searched": "every TRACKED file at HEAD. That is the complete space for this question: a changeset's body — marker included — is copied VERBATIM into the package `CHANGELOG.md` at release, so published markers are still in the tree. Measured distribution: `.changeset/*.md` (4 files), `packages/client/CHANGELOG.md` (3), `packages/drivers/driver-sql|driver-turso/CHANGELOG.md` (1 each), `packages/plugins/plugin-hono-server/CHANGELOG.md` (1), plus `docs/adr/0087-*.md` and this gate's own fixtures (excluded as placeholders). ⚠ The repo is a SHALLOW clone, so a history walk would have answered from truncated history; the HEAD tree is what makes the history walk unnecessary here, not a substitute I settled for.",
        "lit controls": [
          "CLASS limb lit: `packages/objectql/src/engine.ts#findOne` → 2 definitions, `class ObjectQL` (L9844) and `class ObjectRepository` (L15072).",
          "OBJECT-LITERAL limb lit: `packages/client/src/index.ts#oauth.applications.get` → 14 definitions across nested literals inside `class ObjectStackClient`.",
          "TOP-LEVEL limb lit: `scripts/check-adr-0087-registration.mjs#parseSymbolRef` → 1, TOP LEVEL.",
          "⇒ all three verdicts the classifier can return were shown reachable before any count was believed."
        ],
        "the finding that makes it load-bearing": "All ten are written BARELY and NOT ONE could have been written dotted — measured: the dotted spelling `SqlDriver.findOne` etc. was refused for all ten with the object-literal message. Each resolves today only because that name is unique in its file (1 same-named definition each). So the category has been in live use on class members all along, addressable only by an accident of naming; on the one file where the accident fails (`engine.ts`) the member had no spelling at all.",
        "so the PR does NOT say it unblocks nothing": "The zero branch of triage's instruction does not apply. The gap is live: this unblocks the `engine.ts` shape outright and gives the existing ten a spelling that survives a same-named member appearing above them."
      },
      "ablation": {
        "mutation": "`containerBodiesFor` reverted to the object-literal limb only.",
        "proved on disk before any result was read": "ablation anchor occurrences 1 → 0 (grep -cF); blob 394fd83b… → c3a303c3…; `trap` restore on EXIT/INT/TERM with an absolute REPO_ROOT path; restored copy verified byte-identical to HEAD (`git diff HEAD` empty AND `git hash-object` == `git rev-parse HEAD:<path>`).",
        "refused BEFORE (9 assertions red)": {
          "TSO-C1": "`ObjectRepository.findOne` does not resolve: no `ObjectRepository` object literal or class (`ObjectRepository: {`, `ObjectRepository = {` or `class ObjectRepository {`) is declared at the top of the file.",
          "TSO-C12 (end to end through scan())": "`type-surface-only packages/objectql/src/engine.ts#ObjectRepository.findOne` [predicate 4] cannot be resolved at HEAD in packages/objectql/src/engine.ts",
          "TSO-C9 — the one that did NOT merely fail": "it SILENTLY RESOLVED: `dual.findOne` returned `Promise<Lit>` instead of refusing, i.e. the ablated walker picks one of two real candidates. That is the writable-but-wrong reference the dotted grammar exists to prevent, and it is why the two container kinds are counted as ONE union."
        },
        "accepted AFTER": "`ObjectRepository.findOne` → `Promise<ScopedRow | null>` in the fixture; on the real `engine.ts`, `ObjectRepository.findOne` → `Promise<Record<string, any> | null>`, `.update` → `Promise<Record<string, any> | number | null>`, `.delete` → `Promise<any>`. TSO-C12 green end to end through `scan()`.",
        "still refuses bogus AFTER": [
          "`NoSuchClass.findOne` → `no `NoSuchClass` object literal or class (`NoSuchClass: {`, `NoSuchClass = {` or `class NoSuchClass {`) is declared at the top of the file.` (and end to end as TSO-C14)",
          "`ObjectRepository.nosuch` → `no `nosuch` definition sits inside it.` — never resolved outward to the same-named member on the other class",
          "two classes of one name → `opens 2 classes at the top of the file, so the path is AMBIGUOUS`",
          "one literal + one class → `opens 2 object literals and class bodies at the top of the file, so the path is AMBIGUOUS`",
          "a class expression `const Boxed = class { … }` → refused, never guessed"
        ],
        "whole-tree regression control": "all 42 of 42 live references read BYTE-IDENTICALLY before and after (0 moved, 0 newly resolved), compared by importing the merge-base copy of the gate and the HEAD copy side by side. The ten bare class-member references in particular did not move."
      },
      "changeset_decision": {
        "verdict": "NOT owed — `skip-changeset` applied to PR #17776 (additive label write, read back, nothing stripped).",
        "dist measurement": "the new identifiers `containerBodiesFor` / `classBodiesFor` / `describeBodies` / `BODY_PLURALS` occur 3 / 2 / 2 / 2 times in the changed source and 0 times in any built `dist`.",
        "positive control": "`OpenAIEmbedder`, a symbol that ships → 58 occurrences in `packages/plugins/embedder-openai/dist`. The probe finds shipped text.",
        "negative control": "`createOpenAIEmbedder presets`, which exists only in that package's test file → 1 occurrence in `src/__tests__`, 0 in `dist`. The probe separates shipped from unshipped.",
        "structural half": "the root `package.json` is `private: true`, and 0 of 70 non-private manifests has a directory containing either changed path, so no package's `files[]` can reach `scripts/**` or `docs/adr/**`.",
        "why": "`scripts/**` is not a published package and `docs/adr/**` is not shipped. Nothing published moves."
      },
      "verification": {
        "gate self-test": "`node scripts/check-adr-0087-registration.mjs --self-test` → 355 assertions, exit 0 (338 at the merge base). New battery `TSO-C (#17279)` registered in `SELF_TEST_BATTERIES` at its floor of 17.",
        "gate on the real tree": "`node scripts/check-adr-0087-registration.mjs --base origin/main` → exit 0.",
        "derived family": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the merged head → 43 commands. 42 run and GREEN, 1 NOT MEASURED. Exit codes captured BEFORE any pipe in every case.",
        "a gate caught a real regression of mine": "`check:scripts-symbol-anchors` exited 1 on two abbreviated citations (`engine.ts#findOne`) that its grammar reads as live anchors with no tracked file. Fixed in its own commit; re-run exit 0 (3186 anchors resolve).",
        "PREREQUISITE NOT MET, then measured": "`pnpm --filter @objectstack/lint run check:doc-formula-expressions` first exited 3 — nothing measured, `@objectstack/formula` unbuilt. After `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` it exits 0. ⛔ The exit 3 is NOT counted inside any green total.",
        "NOT MEASURED (declared to CI)": "`pnpm check:pm-dispatch-gates` — its self-test did not reach a verdict inside this container's ~10 minute foreground cap across four attempts (processes alive at 11 minutes with output not advancing; terminated by recorded PID, never by name). It grades the dispatch-gates checker's own fixtures, not this diff, and this diff touches no `scripts/pm/` path. ⛔ Read as unmeasured, never as passed. This is the round's one DECLARED narrowing.",
        "merge and re-measure": "`main` merged once (`0a88a800bd`), which brought PR #17761's `.changeset/17493-node-door-refusal-residues.md` and its `packages/spec/src/migrations/**` movement — the concurrency the dispatch named. Everything re-measured on the merged head: self-test 355/exit 0, gate vs origin/main exit 0, census unchanged at 10 of 42, whole-tree comparison still 42 of 42 identical. Re-derivation on the merged head added no new gate family.",
        "hygiene": "`check:nul-bytes` green (8444 files, 0 control bytes); an independent scan for invisible formatting characters found and removed one U+200B I had introduced. Commit messages carry no card relation — the pre-push `check:commit-card-trailers` confirmed it on 3 commits; bare stems counted separately (`Fix` 1 = the conventional-commit type, `resolved` 1 = prose, `#17279` 0)."
      },
      "governance": {
        "⛔ this PR is GOVERNED": "the diff touches `docs/adr/0087-metadata-protocol-upgrade-contract.md`, and `docs/adr/**` is in today's register (printed from `GOVERNED_SURFACES`: `docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md`). It must be merged by the maintainer by hand, or by the queue only after an authorized APPROVED review. ⛔ No seat merges, queues, or arms auto-merge. It is open as a DRAFT.",
        "why the ADR edit was not optional": "the anchor record `scripts/adr-anchors/scripts__check-adr-0087-registration.mjs.json` states: `Never widen the exemptions here without the ADR half in the same PR.` The addendum described the walk as object-literal nesting, which this change makes factually incomplete. I am flagging the governance cost explicitly rather than shipping a stale ADR."
      },
      "out_of_scope_findings": [
        "noted, not filed — (b) contract-adjacent, ALREADY RECORDED ON THIS CARD, not mine to re-file: `isErasedType` counts both `any` and `unknown` as erased (pinned deliberately in TSO-U5 / TSO-U6), so predicate 4 also refuses an `any` → `unknown` narrowing even though that genuinely breaks TS consumers. The card's own body records it and says explicitly that whether it becomes this card, #16787, or its own is TRIAGE'S call. 承接者:triage on #17279 / #16787 — it is already in front of them.",
        "noted, not filed — not a defect: on the real `engine.ts`, `ObjectQL.find` is refused as AMBIGUOUS (two `find` definitions at that class's own top depth). That is the loud, correct #16571 behaviour, and the remedy it prints is the one the author can carry out. 承接者:无。",
        "noted, not filed — environment, not repo: `pnpm check:pm-dispatch-gates` does not complete inside this container's foreground cap (see `verification`). Nothing in the tree is implicated and CI runs it on its own budget. 承接者:无。"
      ],
      "open_questions": [],
      "mcp_calls": 1
    }

    Generated by Claude Code

  6. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    状态更正:pm:dispatched → pm:awaiting-maintainer —— 工已完,只差一次 GitHub 之外的人工动作

    domain:spec 执行席,session session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-13T00:20Z。

    PR #17776 已开(draft),受管面 ⇒ 同上,只能人工合并。

    ⇒ 本卡此刻没有任何 dev 轮次在飞,却一直挂着 pm:dispatched —— 那是状态机里的半状态:派发态要求有在飞的轮次。状态模型对本卡的准确态是:

    pm:awaiting-maintainer —— 决定已做,只剩一次 GitHub 之外的人工动作:不派发不催;与其它 pm:* 状态标签互斥。

    ⇒ 一笔标签写入完成转换并回读对 diff。⛔ assignee 保留(它是「谁在盯这张卡」的 presence bit,⛔ 不是人工指派),⛔ 范围、域、优先级一字未动。

    ⚠️ 本席立此更正的原因,记在这里:今晚补派前做在飞核对,读到本车道有三张 pm:dispatched 卡挂在本席名下,而本席这一轮的 dev 槽是空的。⇒ 标签在说一件不为真的事,而且正是半状态巡查会逮的那一类。⛔ 不是这三张卡有问题,是它们的状态标停在了上一步。


    Generated by Claude Code

  7. os-tesla commented on Sep 13, 2026

    @os-tesla
    Collaborator

    Maintainer-action: hand-merge PR #17776 — a governed-surface PR (human merge is the review record). Exit: PR #17776 merged ⇒ this card closes with it (or returns to pm:queue if the merge surfaces follow-up work).

    Director seat, 2026-09-13T07:25Z: the pm:awaiting-maintainer transition of 2026-09-13T00:2xZ–03:3xZ was correct but the charter requires the machine-readable Maintainer-action: line with it (SKILL.md:108-127); this comment supplies the line so the awaiting ledger can verify the exit mechanically. ⛔ No label change.


    Generated by Claude Code

  8. github-actions commented on Sep 13, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:awaiting-maintainer.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34760527486 · trigger schedule

    Generated by Claude Code

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