Skip to content

fix(check-adr-0087): walk a dotted member path through a class body - #17776

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-17279-type-surface-only-class-member-path
Sep 13, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-17279-type-surface-only-class-member-path

Conversation

@os-bill

@os-bill os-bill commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17279

The defect

PR #15724 closed #15627 by widening a type-surface-only reference to a dotted member path, so an author can name a nested member instead of a bare identifier. The walker it added narrows each leading segment through an object literal only. A member declared on a class therefore has no dotted spelling, and the bare fallback is "the first same-named definition in the file" — which is the defect #15627 was filed on, surviving its own fix on the container kind that fix did not cover.

Reproduced at the merge base on the card's own evidence, packages/objectql/src/engine.ts (read-only here, not edited):

spelling before after
packages/objectql/src/engine.ts#delete (bare) Promise<boolean | number>ObjectQL.delete, which the card's diff never touched unchanged
packages/objectql/src/engine.ts#ObjectRepository.delete refused: no `ObjectRepository` object literal (`ObjectRepository: {` or `ObjectRepository = {`) is declared at the top of the file Promise<any>
packages/objectql/src/engine.ts#ObjectRepository.findOne same refusal Promise<Record<string, any> | null>
packages/objectql/src/engine.ts#ObjectRepository.update same refusal Promise<Record<string, any> | number | null>

The first two rows are the load-bearing pair: the two spellings reach different members (Promise<boolean | number> vs Promise<any>), so the bare one is not a usable substitute — it answers a true sentence about the wrong member.

Lit control for that probe: the object-literal case #15724 rescued, packages/client/src/index.ts#oauth.applications.get, resolves to Promise<OAuthApplication> both before and after. A probe that found nothing for classes was one that could find something for literals.

⭐ The population, established before fixing (triage's gate)

Triage asked how many published narrowings sit on class members today, and made the answer decide how this PR is written. It is not zero.

Method: enumerate every adr-0087: not-required (type-surface-only …) marker in the tree with git grep (markers are copied verbatim from .changeset/*.md into each package's CHANGELOG.md, so the tree at HEAD carries the published ones too), split the comma-separated reference lists, then 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.

Space searched: every tracked file at HEAD — which is where markers live: .changeset/*.md (4 files), packages/*/CHANGELOG.md (4 files), docs/adr/0087-*.md and this gate's own fixtures. Placeholder references (path/to/file.ts#Symbol and the angle-bracket forms) were excluded.

Result — 42 real references:

container of the named symbol count
object literal 30
class 10
top level 1
not a member definition (a top-level type name) 1

The ten class members:

  • 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

Every one of the ten is written BARELY, and not one of them could have been written dotted. Each resolves today only because that name happens to be unique in its file (measured: 1 same-named definition each); the dotted spelling SqlDriver.findOne was refused for all ten with the object-literal message. So the category has been in live use on class members all along, addressable only by an accident of naming — and on the one file where the accident does not hold, engine.ts, the member had no spelling at all.

Lit controls for the census probe (a zero is a reading only if the instrument could have come back the other way):

  • class: packages/objectql/src/engine.ts#findOne → 2 definitions, class ObjectQL (L9844) and class ObjectRepository (L15072). Lit.
  • object literal: packages/client/src/index.ts#oauth.applications.get → 14 definitions across nested literals inside class ObjectStackClient. Lit.
  • top level: scripts/check-adr-0087-registration.mjs#parseSymbolRef → 1, TOP LEVEL. Lit.

⇒ The gap is live, not latent. This PR unblocks the engine.ts shape outright, and gives the ten existing references a spelling that survives a same-named member being added above them.

The change

resolveMemberPath walks each leading segment through containerBodiesFor, the union of objectLiteralBodiesFor and the new classBodiesFor. A union, not two passes: a segment naming one literal and one class is AMBIGUOUS exactly as two literals are, and counting the kinds separately would let it through as "one of each".

Bare references are untouched. Nothing about what the marker means changed, and nothing beyond naming a class member was widened.

⭐ Ablation — it can fail, and it still refuses what it should

Removing the class limb from the union (containerBodiesFor back to object literals only), proven on disk before reading any result — anchor occurrences 1 → 0, blob 394fd83bc3a303c3, restored afterwards and verified byte-identical to HEAD — turns 9 assertions red:

  • TSO-C1 reads back the exact original refusal: no `ObjectRepository` object literal or class (…) is declared at the top of the file.
  • TSO-C12 (end to end, through scan()) fails as [predicate 4] cannot be resolved at HEAD in packages/objectql/src/engine.ts.
  • TSO-C9, the union pin, does not merely fail — it silently resolves: dual.findOne returns Promise<Lit> instead of refusing, i.e. the ablated walker picks one of two real candidates. That is the "writable but wrong" reference the whole dotted grammar exists to prevent, and it is why the union is counted as one set.

Still refused after the change, so the gate did not simply become permissive:

  • NoSuchClass.findOneno `NoSuchClass` object literal or class (`NoSuchClass: {`, `NoSuchClass = {` or `class NoSuchClass {`) is declared at the top of the file (and end to end, TSO-C14).
  • ObjectRepository.nosuchno `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 …, so the path is AMBIGUOUS.
  • one literal + one class → opens 2 object literals and class bodies …, so the path is AMBIGUOUS.
  • a class expression → refused, never guessed.

Whole-tree regression control: all 42 of 42 live references read byte-identically before and after — 0 moved, 0 newly resolved. The ten bare class-member references in particular did not move.

Changeset — measured, not assumed

skip-changeset. Measurement, with both controls, on a real build:

  • Subject: 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 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 files[] can reach scripts/** or docs/adr/**.

⇒ Nothing published moves.

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

⛔ Governed surface — the maintainer merges this by hand

The diff touches docs/adr/0087-metadata-protocol-upgrade-contract.md, and docs/adr/** is in today's register (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md). The ADR edit is not optional: the anchor record for this script states "Never widen the exemptions here without the ADR half in the same PR", and the addendum described the walk as object-literal nesting. ⛔ No seat merges, queues, or arms auto-merge on this PR.

维护者速读(草稿)

改了什么type-surface-only 标记里的点分成员路径,原先只能穿过对象字面量;现在也能穿过类体,于是声明在类方法上的类型收窄终于能在它自己的标记里被点名。裸标识符的含义一个字节都没动。

为什么改 — 这是 #15627 的残留:它的修复只覆盖了一种容器。树上已有 10 条活的引用落在类成员上(SqlDriver / TursoDriver 各 5 条),全部靠"这个名字在该文件里恰好唯一"才能解析;一旦不唯一(engine.ts 就是),作者只剩两条路——写一个指向别的成员的错标记,或者丢掉 **BREAKING** 去躲开判据。后者正是 #13080 记录的侵蚀。

风险与代价(含回滚) — 风险面是这一个门禁脚本的解析行为。全树 42 条现存引用改动前后逐条比对完全一致,消融显示去掉类分支会让 9 条断言转红、其中 1 条会变成"静默挑一个"。回滚 = revert 本 PR,门禁回到今天的行为,无数据迁移、无发布物变动。

席位意见 — (留空,待维护者)

你要做的 — 本 PR 触及 docs/adr/**(受管面),⛔ 不进合并队列、不开自动合并,需要你手动合并或给出授权的 APPROVED 评审。

Verification

  • node scripts/check-adr-0087-registration.mjs --self-test355 assertions, exit 0 (338 at the merge base; +17 in the new TSO-C battery, registered at its floor in SELF_TEST_BATTERIES).
  • node scripts/check-adr-0087-registration.mjs --base origin/main → exit 0.
  • Derived family via node scripts/pm/dispatch-gates.mjs --commands on the merged head: 43 commands — 42 run and green, 1 NOT MEASURED.
    • check:scripts-symbol-anchors caught a real regression of mine (abbreviated engine.ts#findOne prose read as a live anchor) — fixed in its own commit and re-run green.
    • check:doc-formula-expressions first exited 3 (PREREQUISITE NOT MET — nothing measured) because @objectstack/formula was unbuilt; after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint it exits 0.
    • NOT MEASURED: pnpm check:pm-dispatch-gates — its self-test did not reach a verdict inside this container's foreground window across four attempts (still running after 11 minutes, no output advancing), so it is declared to CI rather than reported as green. It grades the dispatch-gates checker's own fixtures, not this diff, and this diff touches no scripts/pm/ path. ⛔ Read this as unmeasured, not as passed.
  • main merged once (0a88a800bd, which brought PR docs(spec): name the node slot in the structural-condition ruling and its ADR-0087 entry #17761's packages/spec/src/migrations/** changeset) and everything above re-measured on the merged head: self-test 355/0, census unchanged at 10 of 42, whole-tree comparison still 42 of 42 identical.

Generated by Claude Code

`resolveMemberPath` narrowed each leading segment through an object literal
only, so a member declared on a class had no dotted spelling: the walk answered
"no `X` object literal is declared" and the bare fallback resolved to the first
same-named definition in the file.

Segments now resolve through an object literal OR a class declaration, counted
as one union so a name opening both is refused as AMBIGUOUS rather than picked
between. The top-depth rule and every bare reference are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…or a class

The anchored invariant on `scripts/check-adr-0087-registration.mjs` requires the
ADR half of any widening in the same PR. The addendum described the walk as
object-literal nesting; it now names both containers, and the author-facing
remedy the gate prints shows the class spelling alongside the literal one.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…w prose

`check:scripts-symbol-anchors` reads `engine.ts#findOne` as a real anchor and
finds no tracked file at that path. The two new citations now use the full
repo-relative path, or an elided one that is not anchor-shaped.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 12, 2026
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 13, 2026 08:07
@os-zhuang
os-zhuang requested a review from hotlong as a code owner September 13, 2026 08:07
@os-zhuang
os-zhuang enabled auto-merge September 13, 2026 08:07
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit cc1b8ac Sep 13, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17279-type-surface-only-class-member-path branch September 13, 2026 08:41
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…rding it (objectstack-ai#17766)

Fixes objectstack-ai#17149

A `Claim:` comment that parses to ZERO branches is now reported as a
MALFORMED claim instead of being discarded. The deliverable is triage
5620657752's, verbatim: 「⛔ do not fix the third spelling. **A parser
that silently yields zero must say so** — an unparsed claim is an
*unclassified* result, ⛔ never a *no*.」 Option 1 on the card (widening
`claimedBranches` for the inline spelling) is **not** done here, and the
branch reader's accept set is byte-identical — pinned as a case, because
that is what keeps the NEXT unrecognised spelling loud on its first
occurrence.

## The mechanism

`governingClaim` picked the newest claim comment from which at least one
branch parses, and `if (branches.length === 0) return;` threw the rest
away. A discard is indistinguishable from an absence, so governance fell
back to an older claim and said nothing.

`claimGovernance(commentRows)` is now the two-part reading:

- `governing` — what `governingClaim` has always returned.
`governingClaim` is a one-line wrapper over it, so the ~18 readers that
legitimately need only branches are untouched (proved below: the
self-test's 3616 pre-existing cases pass unchanged on the refactor
alone).
- `malformed` — the NEWEST claim-shaped comment when it parses to zero
branches, carrying its comment id, its timestamp, and the `created_at`
of whatever governance fell back to.

Recency is written once and both halves call it, so the two readings can
never disagree about which comment is current.

## Every reader says so

| reader | before | after |
|---|---|---|
| H20 / H27 dispatch liveness | probed the superseded branch, or
(nothing parses) went silent as "no claim" | **H60**, a new `state`-band
row, fires for every open `pm:dispatched` card, naming the comment id,
what governance did instead, and the remedy |
| `check-clause2-carriers` declaration limb | read the superseded
claim's `Clause-②` line as if current | `cardDeclaration` returns
`claim-branch-unparsed` **before** any line is read, from any comment
(including the objectstack-ai#17366 correction path) |
| `--pair N` | exit 4 with a verdict taken from the wrong comment |
**exit 2 (UNJUDGED)**, with the whole reading printed |
| the fold/lane rosters (H37/H38) | built on `governing` | unchanged —
they need branches only, and H60 is the row that says the roster may
rest on a superseded claim |

**The exit code is the file's own, not a new one.** Its table already
reads 2 as UNJUDGED — "an unread carrier is NOT a bare carrier and an
unread thread is NOT an absent declaration (objectstack-ai#4690)" — and an
unresolvable carrier is exactly that. Rendering it as 4 would make an
unclassified result an adverse verdict, which is the reading the card
refuses by name. Rendering it as 0-with-a-message is what the whole file
exists against.

## The three questions the card asked

### 1. Where does the guard belong — carriers, half-states, or both?

**Both, from one source.** The state is produced once in
`check-half-states.mjs` (which owns `CLAIM_COMMENT_MARKER` and
`claimedBranches`) and consumed in both files. A second detector in the
carriers file is the drift that file's own docblocks refuse by name
("imported rather than restated ... so the two readers cannot drift").

### 2. Which end?

Option 2 only. See the four-axis reading below.

### 3. Is the fleet's claim template the cause? — MEASURED on the live
board

Read 2026-09-12T03:4xZ, REST, repo-scoped, over every open
`pm:dispatched` card:

- **28** open `pm:dispatched` cards.
- **5** of them (18%) have a newest claim-shaped comment that parses to
ZERO branches: **objectstack-ai#16310, objectstack-ai#16268, objectstack-ai#16251, objectstack-ai#16175, objectstack-ai#15234**.
- 22 parse; 1 (objectstack-ai#13597) carries no claim-shaped comment at all.

Four of the five claims were posted by ONE session
(`session_012GKcPZbMoGq7WPzKLfRBTU`, the `domain:devx` execution PM
seat) within **three seconds** of each other, 02:53:05Z–02:53:08Z, all
in the same shape:

```
Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16310-orphan-locale-key-gateable
Clause-②: yes
```

That is not a run of typos; it is a template emitting a carrier no
reader accepts. **objectstack-ai#16175 is the silent-fallback shape, live today**: its
newest claim (5642984850, 2026-09-12) parses to zero, so governance
falls back to its 2026-09-06 claim (5557414924) — which names a
**different** branch, `claude/issue-16175-regen-sibling-stale-rules`
against the current `claude/issue-16175-staleness-mtime-false-refusal`.
Every downstream reader is probing the wrong ref, and the two `Clause-②`
values happen to AGREE, which is the objectstack-ai#16589 near-miss recorded on objectstack-ai#16322
reproducing itself.

⛔ No `.claude/**` edit is made from this card, per the dispatch. The
template finding is handed to the skills-lane seat; after this PR lands,
those five cards are visible rather than silent, which is the point.

## Four-axis reading of option 1 vs option 2

- **实际业务需求** — measured, not speculative: 5 live carriers today, plus
the objectstack-ai#16322 cost (two rounds, a director re-review, a re-issued claim)
and its near-miss sibling. Option 1 addresses the one spelling in front
of us; option 2 addresses the population that produces them.
- **项目长远合理性** — option 1 is the treadmill the card names and objectstack-ai#16170
already bought once; each widening buys one spelling and leaves the next
silent. Contract-first says the defect is at the reader's CONTRACT (a
two-valued answer where three states exist), not in its accept set.
- **防 AI 写代码犯错** — decisive here. Option 1 is consumer-side tolerance —
the exact shape the axis forbids, and its failure mode is the silent
one. Option 2 is a loud refusal at read time: a claim written in a shape
the protocol does not accept is refused with the remedy named, and the
seat cannot declare a claim the tooling does not honour.
- **创业阶段不扩散需求** — option 2 adds no capability surface: one new reading
of data already in hand, no new request, no new exit code, no new label
written. Option 1 would grow the accept set permanently for a spelling
the standing rule already forbids.

⇒ Option 2, on all four. Option 1 is deliberately NOT also done: doing
both would let the accept set absorb the measured spelling and leave the
new state unexercised on the live board, which is the one way to ship
this fix and still not know whether it works.

## Verification

All exit codes captured before any pipe (`cmd > log 2>&1; EXIT=$?`).

| command | verdict line | exit |
|---|---|---|
| `pnpm check:pm-half-states` | `✓ check-half-states self-test: 3656
cases pass.` (was 3616) | 0 |
| `pnpm check:pm-clause2-carriers` | `✓ check-clause2-carriers
self-test: 493 cases pass (...)` (was 465) | 0 |
| `pnpm check:pm-dispatch-gates` | `✓ dispatch-gates self-test: 1678
cases pass.` | 0 |
| the derived union, 41 commands | `✓ dispatch-gates --ran: 41 derived
famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all
41 recorded an exit code and none of them is 3).` | all 0 |

The union was re-derived and re-run **after** the final commit, on head
`b2b55c2b4` (`git rev-parse --short HEAD`), with `--repo
objectstack-ai/objectstack` asserted. The first derivation printed a
STALE TREE clause naming `check-skill-line-ratchet.mjs` and
`check-widening-tells.mjs` (PR objectstack-ai#17760 had landed); `origin/main` was
merged in, the list re-derived byte-identically at 41, and every command
re-run on the merged head. Reconciliation was fed `command :: exit N`
lines so the zero is derived, not claimed.

⚠️ NOT MEASURED, and named rather than implied: the 47 artifact-roster
families, the 11 declared-wide-population families, the 4 families
taking a value from the workflow, and the 1 path-scheduled CI job are
each outside the derived total — CI's, not this run's.

### Ablation — the new state can actually fail

Committed first, then mutated on disk, then restored; the mutation and
the restore are both proved by `git hash-object` against the HEAD blob
rather than by an exit code.

- Mutation: the pre-fix silent discard restored (`malformed` never
populated). Anchor occurrences 1 → 0, file hash `f9f869cd` → `013b485b`.
- Result: **20 of 3656** half-states cases and **14** carriers cases
turn RED, every one of them in the new batteries. Direction: turns red,
as pre-registered.
- Restore: `git checkout HEAD -- scripts/pm/check-half-states.mjs`; `git
diff HEAD` empty, hash back to `f9f869cd`. The script carries `trap ...
EXIT INT TERM`.

## Live verification — the fix, run against the real board

Both sweeps were run on the merged head, 2026-09-12T04:0xZ.

**`check-half-states.mjs` (sweep, exit 0)** emits exactly **5** H60 rows
— the same five cards the independent REST census found, arrived at
through the fix's own code path: objectstack-ai#15234, objectstack-ai#16175, objectstack-ai#16251, objectstack-ai#16268, objectstack-ai#16310.
Both sentence variants fire live: objectstack-ai#15234 gets the "NOTHING governs this
card" reading, objectstack-ai#16175 gets "governance SILENTLY FELL BACK to an OLDER
claim (2026-09-06...)".

**`check-clause2-carriers.mjs` (sweep, exit 2)** reports **5 of 18
card/PR pairs UNJUDGED** — and two of them (objectstack-ai#15627 via PR objectstack-ai#17776, objectstack-ai#16565
via PR objectstack-ai#17310) are cards the `pm:dispatched` census could not see,
because they are not in that population. Before this change the sweep
exited 0 on all of them.

**The before/after, measured rather than reasoned** — the base tree at
`813f8e9f` materialised with `git archive` and its `cardDeclaration` run
against the same live comment rows:

| card | BEFORE | AFTER |
|---|---|---|
| objectstack-ai#16565 | `declared` `no` (`"Clause-②: no"`) | `claim-branch-unparsed`
|
| objectstack-ai#16175 | `declared` `no` (`"Clause-②: no"`) | `claim-branch-unparsed`
|
| objectstack-ai#15627 | `missing` | `claim-branch-unparsed` |

Two of the three read as a confident `Clause-②: no` taken off a comment
whose governance was never established — the objectstack-ai#16589 near-miss shape,
twice, on today's board. The third read as `missing`, which sends the
seat looking for a declaration line that is already written. Neither was
a reading anybody could have found without opening the card by hand.

## Fixtures

The measured bodies are quoted, never paraphrased, so a future widening
cannot make these batteries pass by accident:

- objectstack-ai#16322's two claims (5593513389 `Clause-②: no`, 5594909614 `Clause-②:
yes`) — the inline spelling, and the two values DISAGREE, which is why
reading the wrong one was a wrong answer and not merely an unlucky one.
⚠️ Re-measured: the OLDER claim is branchless too, so on that card
nothing parsed at all and `cardDeclaration`'s `pool = claimRows`
fallback read the FIRST claim in thread order — a second silent fallback
the card did not name, and the same fix reaches it.
- The live 2026-09-12 specimen on objectstack-ai#16175, with its different-branch
fallback.
- objectstack-ai#16170's bulleted directive still parsing (its pin stays green,
untouched at the H20 branch battery).
- Controls: a well-formed newest claim still governs; an older
branchless claim beside a well-formed newest one raises nothing; no
claim comment at all still reads `absent`; an unreadable thread still
reads `unreadable`.
- The negative pins that keep this a STATE: the inline spelling still
parses to ZERO, and `CLAIM_COMMENT_MARKER` still matches it.

## Acceptance notes

- `skip-changeset` applies: `scripts/pm/**` ships in no package
(fast-track path, no measurement owed).
- `Clause-②: no`, as the claim declares — nothing published moves.
- Scope held: no H22 docblock prose (objectstack-ai#17626 owns it), no widening tells
(objectstack-ai#17618), no `.md`, no `.claude/**`.
- H60 takes the `state` band, beside H34 — H34 reads a claim whose
SEPARATOR the marker refuses, this one reads a claim whose BRANCH line
the directive reader refuses; both are a live card contradicting itself,
repaired on the board. ⛔ Not `stall`: the row does not claim the card is
stopped, and the dev may be working perfectly well — what is broken is
the READING.
- ⛔ No age gate on H60, unlike H20's 60 minutes: a branchless claim is
wrong at the instant it is posted and no later sweep frees it, because
the protocol forbids a second `Claim:`. Pinned in both directions.
- noted, not filed: `cardDeclaration`'s `pool = claimRows` path (used
when nothing on the thread parses) picks the FIRST claim comment in
thread order rather than the newest. This PR makes that path unreachable
for the branchless case, so the residue is a recency question on a path
no live shape now reaches. Successor: whoever next touches
`cardDeclaration` — objectstack-ai#17098 is open against that function's
neighbourhood.

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…eir declared return type (objectstack-ai#17876)

Part of objectstack-ai#17690

Clause-②: yes

⚠️ **Deviation from the dispatch order, flagged rather than taken
silently.** The order said to open this with a closing reference to the
card. It opens with `Part of` instead, because **one of the card's nine
doors is not the defect the card describes** and is deliberately left
un-narrowed here (details below). A closing reference would take the
card out of every open-state filter along with that remainder. This is
the reversible direction: the PM seat can edit one word if it decides
the remainder belongs on a new card.

> **Notation.** Type arguments are written with **square** brackets in
this body — `Promise[any[]]`, `Promise[Record[string, any]]` — because
GitHub's body sanitizer eats short angle-bracket fragments. The code and
the changeset carry the real spelling.

## 1. The ordered first act: the number, and the choice

Triage ordered the pricing typecheck before any edit. Taken, on the
whole consumer closure of both packages at the final tree:

```
turbo run typecheck --filter '...@objectstack/driver-sql' --filter '...@objectstack/driver-turso' --concurrency=2 --continue
  Tasks: 115 successful, 115 total     TS errors: 0
```

**Consumer-site count: 11.** Nine in `@objectstack/driver-sql`, two in
`@objectstack/driver-sqlite-wasm` (which overrides none of these doors
and reaches them through `driver-sql`'s declarations), and **zero
anywhere else** — no package outside the three driver packages held a
concrete driver value whose narrowed result it had to narrow.

11 is below objectstack-ai#17277's 18 ⇒ **one PR**, as the three predecessors did. ⛔
Not split.

## 2. The census — the predicate, and both controls

⛔ Not a grep. The predicate parses `IDataDriver` out of
`packages/spec/src/contracts/data-driver.ts` with the TypeScript
compiler API, enumerates every member, then parses each driver class and
compares its **published** return annotation against the declaration.
The `any` test walks the annotation's type AST for `AnyKeyword` nodes,
so it sees a nested `any` exactly as well as a flat one — which is the
one thing objectstack-ai#15267's literal-string predicate could not do.

| | before | after |
|---|---|---|
| population | 35 members (32 methods, 3 properties) | same |
| classes / cells | 6 / 210 | same |
| tally | EXACT 103 · **MASKED 11** · DIFFERENT 4 · INFERRED 20 · ABSENT
72 | EXACT 112 · **MASKED 2** · DIFFERENT 4 · INFERRED 20 · ABSENT 72 |

The `MASKED 11 → 2` and the `EXACT 103 · DIFFERENT 4` columns reproduce
the card's tally independently. My INFERRED/ABSENT split is `20/72`
against the card's `18/74`: a two-cell boundary difference in how
`InMemoryDriver`'s `name` / `version` accessors are classified, ⛔ not a
disagreement about any flagged door.

- **COVERED control (fired):** the honest doors appear as `EXACT` rows
in the same table — `MongoDBDriver.find` / `upsert` / `aggregate` /
`bulkUpdate`, `InMemoryDriver.find` / `upsert`, `RemoteTransport.find` /
`upsert` / `aggregate` / `bulkUpdate`, `SqlDriver.aggregate`,
`TursoDriver.aggregate`. The predicate is shown to have covered them,
not skipped them.
- **DISCRIMINATION counter-control (fired):** `RemoteTransport.find` /
`upsert` / `bulkUpdate` come back `EXACT` and are **not** flagged, while
`RemoteTransport.beginTransaction` in the same class **is**. A predicate
that flagged everything would have flagged all four.
- **The two remaining MASKED rows after this change are exactly
`InMemoryDriver.aggregate` and `InMemoryDriver.bulkCreate`** — the two
doors under objectstack-ai#5499's freeze that triage declined to escalate. ⛔
Untouched, and the census is the receipt that they were not touched.

The seat's re-measured line table was re-verified in my worktree and
**holds on every one of the nine rows**, including both drifts
(`SqlDriver.temporalFilterValue` 13825, `TursoDriver.bulkUpdate` 1606)
and both seat-supplied numbers (`TursoDriver.beginTransaction` 1662,
`RemoteTransport.beginTransaction` 1835).

## 3. The eight doors that landed

| class | door | published | now |
|---|---|---|---|
| `SqlDriver` | `find` | `Promise[any[]]` | `Promise[Record[string,
unknown][]]` |
| `SqlDriver` | `upsert` | `Promise[Record[string, any]]` |
`Promise[Record[string, unknown]]` |
| `SqlDriver` | `bulkUpdate` | `Promise[Record[string, any][]]` |
`Promise[Record[string, unknown][]]` |
| `SqlDriver` | `temporalFilterValue` | `any` | `unknown` |
| `TursoDriver` | `find` (override) | `Promise[any[]]` |
`Promise[Record[string, unknown][]]` |
| `TursoDriver` | `upsert` (override) | `Promise[Record[string, any]]` |
`Promise[Record[string, unknown]]` |
| `TursoDriver` | `bulkUpdate` (override) | `Promise[Record[string,
any][]]` | `Promise[Record[string, unknown][]]` |
| `RemoteTransport` | `beginTransaction` | `Promise[any]` |
`Promise[unknown]` |

Only the signature lines moved in `sql-driver.ts` — four annotations
plus their docblocks, no read/write body — which is the footprint the
dispatch drew against the concurrent round on that file (objectstack-ai#17859).

## 4. ⛔ The ninth door: a falsified premise, stated plainly

**`TursoDriver.beginTransaction` is NOT narrowed here, and it cannot be
by an annotation swap.**

The card reads its declared type off `IDataDriver` (`Promise[unknown]`,
`data-driver.ts:322`). For an **override** that is the wrong
declaration: `TursoDriver extends SqlDriver`, and
`SqlDriver.beginTransaction()` publishes `Promise[Knex.Transaction]` —
**narrower** than the contract, the honest direction, and listed on the
card itself under "Not findings". The base class is the binding
declaration, so swapping the override onto the contract's own type does
not compile. Measured, by doing it:

```
src/turso-driver.ts(1662,18): error TS2416: Property 'beginTransaction' in type 'TursoDriver'
  is not assignable to the same property in base type 'SqlDriver'.
    Type 'Promise[unknown]' is not assignable to type 'Promise[Transaction[any, any[]]]'.
```

So that `any` is **not masking an un-narrowed door**. It is masking a
genuine LSP violation: in remote mode the override returns a libsql
transaction while the inherited declaration promises a knex one. Closing
it needs one of two things, neither of which is an annotation swap:

- **widen `SqlDriver.beginTransaction` to the contract's
`Promise[unknown]`** — measured by doing that too: **+14 further
consumer sites** in these three driver packages alone (11 → 25), and it
is a type-safety **regression** for every `driver-sql` consumer,
reversing exactly the honest narrowing this card's own "Not findings"
section protects;
- **restructure the remote transaction handle** — a runtime change, and
the dispatch says to stop and report rather than edit bodies.

⇒ Left named in the code, not re-masked and not forced with a cast, and
carried to the PM seat as an open question. This is why the header says
`Part of`.

## 5. The type-level pins — and a phantom leg the ablation caught

Both halves per door, in the owning package's own tsc program
(`tsconfig.json` selects `src/**/*`; neither package carries a DEBT /
TEST_DEBT entry). Extended the two existing files of this family rather
than adding new ones.

**Reverse verification, direction predicted before it was run (turn red,
exactly two errors per door, nothing else).** All eight doors put back
to their masked annotations:

```
TS error count: 16    (8 doors x 2 halves)
  sql-driver-doors-declared-types.test.ts    192-197, 201-202
  turso-driver-doors-declared-types.test.ts  185-190, 195-196
```

The restore leg is proven by `git hash-object` equality on all three
mutated files and an empty `git diff HEAD`.

⭐ **The first ablation run caught this card's own lesson inside the
instrument.** With `find()` back at `Promise[any[]]`, the file red
**once**: `Equals` fired and `IsAny` stayed green — because `IsAny[T]`
asks about `T` itself, and the door resolves to `any[]`, not to `any`.
The mandated "a regression reds the file twice" was one half.

I then checked whether the **landed** doors carry it, rather than
assuming they do not. They do: with `aggregate()` put back to its own
historical `Promise[any[]]`, the shipped pin red **once**. Fixed in
place by extending one detector, `ContainsAny`, across every per-door
leg in both files — it asks `IsAny` first and then looks at the row and
the cell, so it is a strict superset and the doors whose regression
shape is a bare `any` lose nothing. Same `aggregate` ablation after the
change: **2 errors**, both halves.

That in-place fix is bounded and declared: same defect class as this
card, mechanical, both files already in this diff, no new verification
surface, and no other open PR claims either file (checked against the 19
open PRs).

## 6. Consumer-site narrowings — 11, ⛔ never a re-mask, ⛔ never a `!`

| file | sites | shape |
|---|--:|---|
| `driver-sql/src/sql-driver.test.ts` | 2 | an id read off a row is
`assert`-narrowed to `string` or `number` before it is passed as one |
| `driver-sql/src/sql-driver-external-remote-name.test.ts` | 4 |
`Array.prototype.find` now answers "or undefined"; the absent arm is
narrowed away before four field reads that used to compile against
nothing |
| `driver-sql/src/sql-driver-13973-canonical-iso-read-door.test.ts` | 3
| a timestamp typed before `Date.parse`; a row id converted before it is
used as a key; the absent arm of a lookup narrowed |
| `driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts` | 2 | the same id
narrowing, reached through the subclass |

## 7. Changeset

`minor` on `@objectstack/driver-sql` and `@objectstack/driver-turso`,
carrying **BREAKING** under the launch-window convention. No runtime
behaviour changes.

**The ADR-0087 marker is derived from this diff, and it is NOT the one
the order predicted.** The prescribed `type-surface-only` is
**unavailable on all eight doors**, refused on two independent legs —
measured by driving the gate, not read off the source:

- **Leg 1, the six record-shaped doors.** Naming `sql-driver.ts#find`:
*"[predicate 4: narrowed-from-erased] is FALSE: at the merge base the
return annotation of `find` was already CONCRETE (`Promise[any[]]`), not
any / unknown / unannotated."* `isErasedType` draws the line at "the
type **is** `any`", never "the type **contains** `any`" — this card's
subject, one layer up.
- **Leg 2, the two unknown-destination doors.** Naming
`remote-transport.ts#beginTransaction`: *"[predicate 4] is false at
HEAD: the return annotation of `beginTransaction` is still
`Promise[unknown]`. This category is for a surface that MOVED OFF an
erased type. One that is still erased narrowed nothing."* `unknown` is a
real narrowing to a consumer — it admits no property read — but the
predicate groups it with `any`. This is the erased-destination wrinkle
triage named.

The two legs are each other's control: same citation form, same marker
grammar, two **different** refusals naming two different revs ⇒ the
probe discriminates rather than rejecting whatever it is handed.
Disposition used: `not-required (no-migration-prescription)`, with both
measurements written into the marker. **BREAKING** is carried, ⛔ not
dropped.

## 8. Verification

| what | result |
|---|---|
| `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | 62
families derived; **all 62 run, every one exit 0** |
| `--ran` reconciliation | `62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN`
— a DERIVED zero, every family carrying its recorded exit code |
| `check:dual-build-cjs-loads` | first run **exit 3, PREREQUISITE NOT
MET** (three packages had no `dist`) — ⛔ not counted as a pass; re-run
**exit 0** after a full `turbo run build` |
| `driver-sql` test | 183 files (172 passed, 11 skipped), 2711 tests
(2550 passed, 161 skipped) |
| `driver-turso` test | 52 files, 1248 tests, all passed |
| `driver-sqlite-wasm` test | 29 files, 518 tests, all passed |
| typecheck | the three driver packages: 0 errors; whole consumer
closure: 115/115 tasks, 0 errors |
| lint | `eslint . --no-inline-config` over the **whole** population —
**6656 files, 0 errors, 0 warnings**, exit 0. Not a narrowed run, so no
narrowing needs proving |
| control characters | `check:nul-bytes` exit 0, plus a direct scan of
all 10 changed paths for the wider control-byte class: no match |

**Tiers.** All three packages spell `test` as a bare `vitest run` and
their `vitest.config.ts` declares no projects — **one tier each, run
unnarrowed**. ⛔ No `--project` filter anywhere in this round (objectstack-ai#17853).
The 11 skipped `driver-sql` files are its live-dialect Postgres/MySQL
matrix, which skips without a live URL — the same lane CI's non-live job
takes. The new cases were confirmed to have actually executed, by name:
the `driver-sql` pin file runs 13 cases (was 8), the `driver-turso` pin
file 10 (was 7).

All readings were taken against this branch at `1c479a33fe`. The lint
and gate figures are from that same tree; nothing has been committed
since.

## Acceptance notes

Findings from this round that are **not** repaired here:

- **`TursoDriver.beginTransaction` masks an LSP violation, not an
annotation.** Section 4. In scope of this card by the card's reckoning,
out of reach of its repair shape. Carried to the PM seat as an open
question with both options priced.
- **noted, not filed — `type-surface-only` is unclaimable by this class,
on two legs; both belong to a family already collected.** Section 7.
Dedup run over the 500 most recently updated cards (numbers 3739-17876,
open and closed), keyword-scanned for `type-surface-only` /
`isErasedType` / `narrowed-from-erased`; control fired — objectstack-ai#17279 came
back and is the card that names both terms.
- **Leg 2 is already on the record, verbatim.** objectstack-ai#17279 carries it under
"An adjacent limitation, measured in the same round, recorded not
filed": `isErasedType` counts both `any` and `unknown` as erased (pinned
on purpose as TSO-U5 / TSO-U6), so predicate 4 refuses an `any` to
`unknown` narrowing. My measurement is a second, independent
confirmation of it, ⛔ not a new finding. Its author handed it to their
PM seat rather than filing, on the grounds that deliberately-pinned
behaviour makes "defect or incomplete" a call no implementer should take
alone. Same reasoning applies here, so it is handed over the same way.
- **Leg 1 is a new limb of that same family, and the gate declares it
deliberate.** The nested-`any` base reading is NOT what objectstack-ai#17279 reports:
that card's identical-looking "already CONCRETE" row comes from
resolving the WRONG same-named member, while leg 1 resolves the right
one and is still refused. But the gate's own header states the line and
its price: *"The line is 'the type IS `any`', never 'the type CONTAINS
`any`' … admitting the broader question costs those gates their
zero-false-positive property … `{ rows: any[] }` at base is a concrete
object type, and a change that alters its members is not this category's
class."* So this is a documented decision, ⛔ not a contract violation
and ⛔ not a defect to file against — it is the same judgement call, and
it goes to the PM seat.
- 承接者: objectstack-ai#17279 (`pm:dispatched`, PR objectstack-ai#17776 in flight, which repairs
symbol RESOLUTION rather than what predicate 4 then reads) and objectstack-ai#16787
(open, the sibling "unclaimable by its own subject" card on a different
predicate pair). Which card absorbs which leg is triage's call, as
objectstack-ai#17279 itself says. ⛔ No comment posted on either from this round.
- **noted, not filed: eleven further files carry per-door `IsAny` legs
of this family** (`driver-memory` x2, `driver-sqlite-wasm` x3,
`driver-turso` x3, `driver-sql` x2, `objectql` x1). Whether each is a
phantom leg depends on that door's declared shape and was not measured —
only the two files in this diff were. 承接者: the next round of this pin
family, or whoever takes the `ContainsAny` finding; the detector is now
written down in both files here for it to copy.
- **noted, not filed: the parameter side of these same doors is still
`any`** (`data: Record[string, any]` on `upsert` and `bulkUpdate`,
`value: any` on `temporalFilterValue`). Deliberately not in this family
and recorded in the pin file's prose: a parameter typed `any` accepts
exactly what one typed `unknown` accepts, so it erodes nothing on the
**caller's** side, which is the axis this card is about. 承接者: no one —
it is recorded as a boundary, not as a defect.

⛔ Not ready for review-flip or auto-merge by any agent seat: landing is
the PM seat's act.

Authored by Claude Code in session `01RuoNSXUbBoWHkNS4AknTrM`
(https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM), dispatched by
the `domain:engine` execution PM seat. Attribution is stated here in
prose because this surface APPENDS its own footer block on every body
edit — the measured behaviour AGENTS.md records — so a footer written
into the text would simply accumulate.


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…correct the residue and D3's pointer (objectstack-ai#17808)

Fixes objectstack-ai#16270

**Why `Fixes` and not `Part of`.** This is the card's last residue. Its
provenance half landed in PR objectstack-ai#17750 (the three documents that asserted
the tab strip now name the record that declares it); the seat's merge
comment recorded that the card stayed open carrying exactly one
correction — this ADR. With it landed, nothing of objectstack-ai#16270 remains.

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

⚠️ **This PR is GOVERNED and stays draft.** `docs/adr/**` is a governed
surface (AGENTS.md Prime Directive 14). ⛔ No seat flips it ready,
enqueues it, or arms auto-merge; a human merge is the review record.
That is the expected end state, not a problem to route around.

---

## What was wrong

ADR-0133's "What this record does not decide", item 1, asserted **in the
present tense** that "Opens on tab-0 Members" is *"declared by none of
its metadata"*, and that the tab ordering is *"either an emergent
property of the renderer or a claim that has gone stale."*

Both halves are false, and were already false on the day they were
written.

**The tab strip is declared metadata.**
`packages/platform-objects/src/pages/sys-organization.page.ts` exports
`SysOrganizationDetailPage` — `type: 'record'`, `kind: 'slotted'`,
`isDefault: true`, for `sys_organization` — whose `slots.tabs` override
carries exactly three `record:related_list` tabs:

| order | label | objectName |
|:--|:--|:--|
| 0 | Members | `sys_member` |
| 1 | Invitations | `sys_invitation` |
| 2 | Teams | `sys_team` |

plugin-auth hands it to the runtime — `auth-plugin.ts` imports it and
declares `pages: [SysOrganizationDetailPage, SysUserDetailPage]`. It is
registered, not a dead export.

**It already existed at `77781151d`, the very commit the ADR cites.**
The contents API for that path at that ref answers **HTTP 200**, blob
`2f56173ff2c84b1ee9fb3324577df71fd78d9b3b`, 4606 bytes. **Lit control:**
the same call for a fabricated sibling path at the same ref answers
**Not Found**. So the 200 is a reading.

## Why a correct measurement produced a wrong conclusion

⭐ **This is the reusable part, and it is what the correction carries.**
The ADR's two greps reproduce **exactly** on my own head, with their own
lit control — they were never the error:

- `git grep -rn relatedList -- packages/platform-objects/src/identity/`
— the two hits on today's tree are the *provenance comments* PR objectstack-ai#17750
added; **zero declarations**, which is what the sentence claimed. Read
the sites, not the count.
- Lit control, same sweep:
`packages/drivers/driver-sql/src/builtin-column-collision.ts` still
hits, so the pattern matches something.
- `relatedLayout` — 7 hits, all prose (3 in ADR-0085, 1 here, 1 in
`field.zod.ts`'s doc comment, 2 in a SKILL.md). Zero object
declarations.

The search was **exhaustive over the wrong space**: scoped to
`src/identity/` and to the `relatedList` key, while the declaration
lives one directory over in `src/pages/`, written in a different
vocabulary — an **assigned Page**, not a field prominence key. A control
proves a probe reaches; it cannot prove the probe is aimed at the right
place.

⚠️ **Neither branch of the disjunction was even available**, measured at
the objectui sha this repo pins (`.objectui-sha` =
`53ded82bf7a494f54e344e19099dbf00854b8694`, read with `git show`, ⛔
nothing there edited):

- `buildDefaultTabs` seeds `items[0]` with `{ label: 'Details', value:
'details', … }` **unconditionally**, so a promoted related list could
never be tab-0.
- `buildDefaultPageSchema` never calls it at all when an assigned page
supplies a `tabs` slot: `if ('tabs' in slots && slots.tabs !==
undefined) { components.push(...toNodeArray(slots.tabs)); }`.

⇒ adding `relatedList: 'primary'` would have been **inert on this
page**, not corrective. "An emergent property of the renderer" is not
merely unproven; it is measurably not what happens.

## Item disposition — it STAYS, as a corrected residue

Asked to choose whether the item stays in "What this record does not
decide" or moves. **It stays**, for three reasons:

1. **The disposition is still true.** This record does not decide the
tab set or its order — that was never the false part. What was false was
the *ground* stated for it. Moving the item would imply the placement
was the error.
2. **The section's preamble stays honest.** It says "⛔ None of it is
resolved here." Still true: this record decides nothing new. A
correction of fact recorded in place is not a decision this ADR now
makes.
3. **A correction belongs beside the reading it corrects.** ⛔ The item
is not deleted — it records a real measurement, and the honest edit says
what corrected it. The original reading is preserved verbatim under "The
reading as originally recorded", including the "emergent property / gone
stale" conclusion, quoted so a reader sees exactly what was overturned.

The item now reads: recorded reading → what corrected it → what the
measurement missed → why neither branch was available → the unchanged
disposition.

## ⚠️ Declared scope expansion — one sentence in D3

The dispatch fenced this round to item 1. **A whole-file sweep found the
same falsehood at a second site**, and I corrected it too rather than
leave a governed record contradicting itself one section away:

> ⚠️ **The tab set and its ordering are NOT declared by this repository,
and this record does not decide them.** See [What this record does not
decide](#what-this-record-does-not-decide).

That sentence is D3's **pointer into the very item being corrected**.
Leaving it would have made the ADR assert the falsehood in its
*decision* section while refuting it in its residue section. It is
corrected minimally: the "not declared" clause is replaced with the
declaring record and its registration, the true half ("this record does
not decide them") is kept word for word, and the pointer is kept.

This is the bounded in-place fix, and all four conditions were measured
before taking it: ① same defect class as the card; ② mechanical, in a
form already pinned by item 1's correction; ③ no other claim holds this
file — all 16 open PRs were enumerated and their file lists read; two
touch `docs/adr/`, namely PR objectstack-ai#17776 (ADR-0087) and PR objectstack-ai#17756 (ADR-0025),
and **zero** touch ADR-0133; ④ same gate family, no new verification
surface.

⭐ The sweep that found it is the same discipline this card is about:
`NOT declared` → 1, `declared by none` → 1, `emergent` → 1, `tab order`
→ 2, on a whitespace-flattened, indent-stripped stream. Exactly two
sites; both corrected.

⛔ Items 2, 3 and 4 of that section are **byte-untouched** (they appear
in the diff only as context lines). No code, no metadata, no other ADR,
nothing under `content/docs/releases/`.

## Verification

⚠️ **Prose probes must be flattened before matching.** On the raw file,
three of the four quotes I needed returned **0** — they wrap across
lines. On a flattened, indent-stripped stream all four return **1**,
with `relatedList` → **2** on the same stream as the lit control proving
the probe reaches.

| probe | raw file | flattened |
|:--|--:|--:|
| `declared by none of its metadata` | 1 | 1 |
| `emergent property of the renderer` | **0** | 1 |
| `no object in ... declares the relatedList prominence key` | **0** | 1
|
| the deep-link-contract sentence | **0** | 1 |
| **lit control** `relatedList` | — | **2** |

**On-disk proof of the edit** (⛔ not the editor's exit code): replaced
text → 0 occurrences each; injected text → present each; the preserved
original reading → still 1 each; `relatedLayout` → 1 as the control on
the same after-stream.

**Gates — all run with the exit code captured BEFORE any pipe** (`cmd >
file 2>&1; EXIT=$?`), the set derived by `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (18 commands; the stderr provenance line
names this repo and commit `310760d225`):

| gate | exit |
|:--|--:|
| `check-adr-symbol-anchors` (+ `--self-test`) | 0 |
| `check-adr-links` (+ `--self-test`) | 0 |
| `check:adr-anchors` | 0 |
| `check:doc-authoring` | 0 |
| `check:nul-bytes` | 0 |
| `check:pm-governed-merges` | 0 |
| `check-closing-keyword-parity` (+ `--self-test`) | 0 |
| `check-ci-filter-parity` | 0 |
| `check-comment-mask-corpus` | 0 |
| `check:cross-package-test-inputs` | 0 |
| `check:driver-memory-census` · `check:refd-timer-probe` ·
`check:watch-hint-literal` | 0 |
| `report-test-timings --self-test` | 0 |

⭐ **The new anchors are genuinely checked — proven by ablation, not by
the green.** `check-adr-symbol-anchors` passes 2079 anchors across 139
records. Renaming the symbol inside my new anchor on disk drove it to
**exit 1** with `[unresolved-symbol]` at **both** new sites; restoring
reproduced the file **byte-identical** (`git hash-object` `e085c37a…`
before and after, with a `trap … EXIT INT TERM` and absolute paths), and
the gate returned to exit 0. On-disk proof preceded reading any result:
injected marker → 2, original spelling → 0.

⚠️ **The two `objectui:` anchors are only judged when a checkout is
available.** Default run: 27 cross-repo anchors reported-not-judged.
With `OBJECTUI_CHECKOUT=/home/user/objectui`: skipped drops to 11 and
mine are not among them ⇒ judged and resolved. Both symbols were
independently confirmed at the pinned sha with `git show`.

⛔ **One gate was NOT MEASURED at first and is now measured.**
`check:doc-formula-expressions` returned **exit 3 — PREREQUISITE NOT
MET** (`@objectstack/formula` and `@objectstack/lint` not built). Exit 3
is not a failure and ⛔ never counts inside a green tally. My file **is**
in its corpus (`ROOTS` includes `docs`; `docs/adr` is not in
`SKIP_PATHS`), so the prerequisite was satisfied and the gate re-run
rather than waived.

**Repo-wide scans are CI's.** No local narrowing is claimed for them.

## Changeset — `skip-changeset`, and this is why

⛔ Measured, not assumed.

- **Target:** `docs/adr/0133-org-management-open-basics.md` resolves to
**no owning package at all** — it sits outside every package directory,
so no `files[]` can reach it.
- Across the workspace: **70 published packages** (12 private skipped, 0
published without a `files[]`). The **union** of every `files[]` entry
is `CHANGELOG.md | README.md | api-surface | dist | json-schema |
liveness | llms.txt | prompts | spec-changes.json | src/**/*.zod.ts`.
Entries naming `docs/adr` or escaping the package dir with `..`: **0**.
- All 139 ADR files live at repo-root `docs/adr/`; **none** is nested
inside a package.
- No build or copy step pulls them in: `git grep docs/adr` over every
`package.json`, the tsup configs and `turbo.json` → **no hits, exit 1**.
**Lit control** on the same probe space: `dist` in those same
`package.json` files → hits, exit 0. So the zero is a reading.
- **Positive control (ships):** `packages/platform-objects/README.md` →
direct `files[]` hit **true**.
- **Negative control (does not):**
`packages/platform-objects/src/identity/invite-entry-toolbar.test.ts` →
direct `files[]` hit **false**.

⇒ nothing published moves, so `skip-changeset` applies — the label's own
documented case in `lint.yml` is a PR that releases nothing. ⚠️ Note for
whoever lands this: the size-labeler's whole-set `PUT` has erased a
seat-applied `skip-changeset` before; the label was applied additively
and read back.

⭐ Deliberately the **opposite** call from PR objectstack-ai#17750 on the same card,
and both are right — because both were measured. There, a comment-only
diff moved published bytes (the new text appeared 4 times inside
`packages/platform-objects/dist`), so a changeset was owed. Here the
file cannot reach a tarball at all.

## 维护者速读(草稿)

**改了什么** — ADR-0133「本记录不决定什么」第 1 条,以及 D3 里指向该条的那一句。原文用现在时断言「组织记录页开在
tab-0 Members」这件事「没有任何元数据声明它」,并推论 tab
顺序要么是「渲染器的涌现属性」,要么是「已经过时的说法」。两半都是假的,而且写下它的那天就已经是假的。

**为什么改** — tab 条本来就是声明出来的:`SysOrganizationDetailPage` 是
`sys_organization` 的 slotted 记录页,`slots.tabs` 里正好三个 related_list ——
Members、Invitations、Teams,顺序如此,由 plugin-auth 注册进运行时。它在 ADR 自己引用的那个
commit `77781151d` 上就已经存在(HTTP 200,带「伪造路径回 Not Found」的对照)。⭐ ADR
的**测量是对的,推论是错的**:搜索被限定在 `src/identity/` 和 `relatedList` 这个键上,而声明在隔壁
`src/pages/`,用的是另一套词汇 ——
一次「把错误的空间穷尽搜索」必然回一个理直气壮的零。这条教训是本次修正真正要留下的东西。原读数**没有删**,原样保留并标注是什么推翻了它。

**风险与代价(含回滚)** — 风险很低:改的是散文,不动任何代码、元数据或其他 ADR;第 2、3、4
条逐字节未动。新引用全部是受门禁校验的符号锚,并用消融证明了它们真的会变红(改名后 exit 1,还原后字节一致、exit 0)。⛔
不发布任何东西,因此 `skip-changeset`(已实测,带正负对照)。回滚 = 直接 revert 这一个
commit,单文件,无下游依赖。⚠️ 需要您注意的只有一处:派发把范围钉在第 1 条,我按「有界就地修」把 D3 里同一句假话也改了 ——
否则同一份受管记录会在决策节说 A、在残留节说非 A。若您认为不该扩,删掉 D3 那一段即可,第 1 条的修正独立成立。

**席位意见** — (留空,待席位补)

**你要做的** — 这是**受管面**,按 Prime Directive 14,只有您手工合并;⛔ 任何 AI 席位都不会把它翻
ready、不入队、不挂 auto-merge。请确认两件事:① D3 的那处扩范围要不要保留;② 第 1
条留在「本记录不决定什么」作为「已更正的残留」是否合您的意 —— 我给的理由是:该条的处置本身没错(本记录确实不决定 tab
顺序),错的是它给出的理由,所以位置不动、理由更正。

## 验收备注

- **noted, not filed** — 观察类,不立卡:`docs/qa/platform-checklist/runs/`
里仍然只有
`README.md`,`identity-auth.org-membership-team-management`(P1)从未对真实
console 执行过。它的 tab 断言是从 cloud ADR-0081 写出来的,没有被观察过。这是清单计划自己的
backlog,不是本树的缺陷。**承接者:** checklist-test lane / 下一次 platform-checklist
运行。
- **noted, not filed** — 命名撞车,不是缺陷:objectui 另有一个手写的 console 区域
`/organizations/:slug`(`OrganizationLayout.tsx`),tabs 是 Members /
Invitations / **Settings**,没有 Teams,且确实 index-redirect 到
members。任何靠点界面来复测本卡的人都可能落到那个界面,然后在任一方向上得出一个很自信的错误答案。**承接者:** 本节本身 ——
下一个读这些文档的人在这里遇到它;PR objectstack-ai#17750 的验收备注已记过一次,这里是它在受管记录这一侧的对应位置。
- **noted, not filed** — 边界记录,不扩类:ADR-0133 的状态行仍是 `Proposed
(2026-09-06)`,等待维护者手工合并这一「受管面的接受动作」。本 PR 不动状态行 ——
那是维护者的动作,不是更正的一部分。**承接者:** 维护者,在合并本 PR 时一并判断。

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants