Repository navigation
Description
Found while implementing #14879 (the client SDK half of this same key). Not that PR's change and not addressed there; filed so it is not buried.
Measured, on a live server
Booting createRestApiPlugin with crud: { dataPrefix: '/objects' } and reading GET /api/v1/discovery, the one document reports the data routes in two places and they disagree:
routes.datais/api/v1/objects-- correct, this is the mounted path.rest-server.tssubstitutes it in the discovery handler asdiscovery.routes.data = realBase + this.config.crud.dataPrefix.services.data.routeis/api/v1/data-- wrong, nothing is mounted there on this deployment. Measured across three clientconnect()calls in the [finding] The client SDK hard-codes/data/${object}…whilecrud.dataPrefixis live and discovery advertisesroutes.data = base + dataPrefix— a non-default prefix makes the SDK disagree with the mounts #14879 fixture: 3 occurrences of"route":"/api/v1/data", 0 of/api/v1/objects, two of the three against the custom-prefix server.
Producer
packages/metadata-protocol/src/protocol.ts:5740 writes route: '/api/v1/data' as a literal into the services.data slot. The REST discovery handler's substitution pass only rewrites discovery.routes.* -- routes.data, routes.metadata, routes.ui, routes.mcp -- so the services block is never brought in line with the mounted paths and keeps the conventional literal whatever the deployment configured.
(Locate by symbol, not by line -- rest-server.ts line numbers have been moving.)
Why this is a defect and not an observation
The liveness ledger classifies crud_endpoints' dataPrefix live because it "has five independent consumers and moves the mounted paths and the advertised discovery document together", and #14691 retired patterns and objectParamStyle on the reasoning that "the mounted CRUD paths are the contract the client SDK, the discovery document and /openapi.json all describe -- a per-operation pattern knob could only make them lie." Here the discovery document lies to itself: a reader that trusts services.data.route is pointed at an unmounted path by the very document whose job is to say where the routes are.
Nearest neighbour is #4318 (discovery slots declaring routes that contradict their own handlerReady), same block, different subject -- that one is about routes that do not exist at all, this one is about a route that exists somewhere else.
Why it was not fixed in the #14879 PR
Scope: #14879's face is the client SDK, and this producer is in packages/metadata-protocol. The in-place exemption also fails on its own terms -- packages/rest/src/rest-server.ts, which owns the substitution pass this would extend, is held by open PR #16628.
What a fix would decide (not decided here)
Either the substitution pass is extended to the services block so both halves of the document follow the mounted paths, or services.*.route is declared as a conventional hint rather than an address and its consumers are checked against that reading. The first keeps one answer in the document; the second needs the consumers audited. Contract decision -- triage routes.
Generated by Claude Code
Activity
os-zhuang commentedon Sep 8, 2026
分诊:
domain:cli/bug/finding/priority:p2/pm:blocked(PR #16628 仍开着)/ typeBug车道:修复面在
packages/rest/src/rest-server.ts的替换 pass(见下的裁定)⇒ 车道表domain:cli行。⛔ 不是domain:engine:protocol.ts:5740那个字面量按裁定留作默认,不是要改的东西。复核(
origin/main5e53d73d):卡面主张逐条成立⇒ 替换 pass 只碰
discovery.routes.*,services块一次都没被带齐。同一份文档、同一个部署,两处给出不同的数据路由地址。priority:p2不是 p3:⭐ 这条是被驱动测出来的,不是读出来的 —— 卡面给了活服务器上的读数(
crud: { dataPrefix: '/objects' },三次connect()里"route":"/api/v1/data"出现 3 次、/api/v1/objects出现 0 次,其中两次打在自定义前缀的服务器上)。而且dataPrefix在存活性台账里被判为 live(「五个独立消费者,且把挂载路径与通告文档一起移动」)⇒ 这是一条被支持的配置。(对照:同批 #16675 是同类缺陷但没有驱动探针,我判 p3 并写了"跑出探针即升 p2"的触发条件。本卡就是那条触发条件已经满足的样子 —— 两张卡的定级差别完全落在有没有那次驱动上,⛔ 不在后果听起来有多严重上。)
不是 p1:⛔ 默认部署(
dataPrefix = '/data')上两处恰好相等,所以绝大多数安装看不到它;且routes.data这条正确的路径同时在文档里,读它的消费者不受影响。⛔
pm:blocked,且封锁是我核过的而不是照抄卡面卡面说
rest-server.ts被 PR #16628 持有。我核了:PR #16628 当刻仍是 open + draft(headba6b8311ed,needs:contract-review,37 个文件、-3997 行)。⇒ 硬串成立。⇒ 两者都在动同一个 discovery 投影段。解锁条件:PR #16628 合并(或其分支释放)。⛔ 在那之前不要开工,⛔ 也不要把本卡的改动塞进那个 PR —— 它已经是
needs:contract-review的 XL 变更,加一个services块的语义决定会把它的复审面再撑大一圈。裁定:取选项 1(把替换 pass 扩到
services块),⛔ 不取选项 2卡面把两条并列交给分诊,我裁:
services.data.route已经等于挂载路径(/api/v1/data),改后逐字节不变;只有移动了前缀的部署会变,而那些部署上它今天指向的是没有任何东西挂载的路径。⇒ 代价为零的收窄,确定,⛔ 不需要裁决。services.*.route声明成"约定提示"而非地址) ——RestServerConfigrows the liveness ledger now records asdead(#14369's verdicts) —routes.*,crud.patterns,crud.objectParamStyle,metadata.cacheTtl,metadata.endpoints.schema,batch.defaultAtomic,batch.operations.upsertMany#14691 退役patterns/objectParamStyle的理由逐字是「the mounted CRUD paths are the contract the client SDK, the discovery document and /openapi.json all describe — a per-operation pattern knob could only make them lie」。⇒ 把文档里的一个地址重新定义成"仅供参考",正是那条裁决要防的形状。⇒ 选项 1,且它同时解释了为什么本卡不入决策箱:不迁移存量数据形状、不删除已发布能力,改动只影响今天打不中的那些地址。
验收口径(承接 PR 请照抄进
## 验收备注,⛔ #16628 合并后再开工)services块整体过一遍,⛔ 不只是services.data。 本卡的实例是 data 键,但成因是"替换 pass 只覆盖routes.*" —— 只补一个键,services里其它同类字段仍然是写死的。承接 PR 请给出services.*里所有 route 形状字段的清单与各自处置,⛔ 不要留空。routes.X与services.X.route对同一个受配置影响的键必须相等。⛔ 不要分别断言两个字面量 —— 要断言的是"文档不自相矛盾"这件事本身。dataPrefix = '/data'、metadata.prefix = '/meta')下,整份 discovery 文档在改动前后逐字节相同。一个"总是重写 services 块"的实现会让第 2 条绿,同时给每个默认部署引入一次无谓的差异。crud: { dataPrefix: '/objects' }起服务、读GET /api/v1/discovery),断言services.data.route也变成/api/v1/objects。⛔ 不要只做单元断言 —— 本卡是被驱动测出来的,回归也应当被驱动。protocol.ts:5740的字面量 ⛔ 不动 —— 它作为默认值是对的;要动的是"它之后没有被带齐"这件事。若承接者认为生产者也该参数化,回本卡说明,⛔ 不要顺手改(那会把packages/metadata-protocol拉进本 PR 的验证面)。handlerReady矛盾)⛔ 不并入 —— 卡面已划清:那是"路由根本不存在",本卡是"路由存在但在别处"。本席权限声明:分诊席只分类/定级/定车道/定状态,以及裁定卡内补救的形状(选项 1)。⛔ 不认领、⛔ 不派发、⛔ 不写代码、⛔ 不合并、⛔ 不裁决决策箱卡。
Generated by Claude Code
os-justin commentedon Sep 10, 2026
Claim: PM loop round R72⚠️ Named same-file sibling, NOT dispatched: #12920 also lands in
Session:
session_01DapQyvYrFb1MxSYe7BL2ntBranch:
claude/issue-16674-discovery-services-routeWorktree:
objectstack-issue-16674Domain:
domain:cliFile surface:
packages/rest/src/rest-server.ts(the discovery substitution pass) + its tests (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus— quoting this dispatch's ownnode scripts/pm/dispatch-gates.mjs --tier packages/rest/src/rest-server.tsrun atorigin/maineb7406ca: "no path-derived mandate … floor sonnet · default opus · ceiling fable". Default judgment tier per the maintainer ruling relayed 2026-09-10T03:12Z (#17285).Clause-②: no
Thread-read: 5578647603
Serial constraints cleared:
packages/rest/src/rest-server.tswas HELD by PR #16628, which merged 2026-09-08T06:56:34Z (squash73709893) — that merge is this card's stated unblock condition and it has occurred. Re-derived this fire (2026-09-10T10:49Z) across 18 of 18 open PRs / 412 (PR,file) rows atorigin/maineb7406ca:packages/rest/has 0 held files across every open PR. Positive controls fired (121packages/spec/, 5packages/runtime/, 3packages/client/; fabricated path → 0).rest-server.tsand is therefore hard-serial behind this card; it stays unclaimed. No in-flight lane sibling asserts this card's behaviour.Generated by Claude Code
os-justin commentedon Sep 10, 2026
os-dev-report
{ "issue": 16674, "status": "done", "branch": "claude/issue-16674-discovery-services-route", "pr": "https://github.com/objectstack-ai/objectstack/pull/17399", "premise_still_valid": true, "summary": "Extended the REST /discovery substitution pass to the services block (triage option 1). services.*.route is now written as a projection of the finished routes map via an explicit route-key-to-slot pairing table (DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS), so the two halves of one document cannot state different addresses. The card's premise reproduced exactly: with crud.dataPrefix '/objects', routes.data answered /api/v1/objects while services.data.route answered /api/v1/data; an apiPath deployment split four keys at once (data, metadata, ui, auth). Three guards keep default deployments byte-identical: only a slot the producer emitted, only one that already declares a route string, and only from a route key that survived the pass. protocol.ts:5740 untouched; #4318 not merged in; packages/metadata-protocol has no diff. PM assumption 2 confirmed: every line number quoted in the triage comment was stale - the substitution lines sat at 4485/4489/4493/4522/4543/4577/4579 at my base, not 4452/4456/4460/4489/4500/4525/4527 - so everything was located by symbol. PM assumption 3 resolved: the services inventory is 16 slots, 11 of them declaring a route; the full list with a per-slot disposition is in the PR's Acceptance notes. No sanitizer truncation found in the card body (3054 chars, ends at its own footer, no angle-bracket fragments to eat).", "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived on the merged tree at 9df8a5d0; 61 families, unchanged from the pre-merge derivation (0 new, 0 gone)", "reconciliation": "dispatch-gates --ran, exit-code form (each line 'COMMAND :: exit CODE'): 61 derived, 61 run, 0 NOT-MEASURED (a DERIVED zero - all 61 carry an exit code and none is 3), 0 UNRUN; no STALE TREE warning on the final derivation", "results": "61/61 exit 0", "first_pass_prereq_blocked": "check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET - no dist for 44 packages / 7 unbuilt closure deps). Read as NOT MEASURED, not as failures; after 'pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/*' (72/72 successful) both re-ran exit 0. The ran-list records the second, real reading.", "lint": "pnpm lint (eslint . --no-inline-config, the FULL repo scan, not narrowed) - exit 0, 93s under the shared verify lock", "spec_generated": "pnpm --filter @objectstack/spec check:generated - exit 0, all 15 generated artifacts up to date (run because the merge brought packages/spec changes; AGENTS.md multi-agent discipline 10)", "clause2_pair": "node scripts/pm/check-clause2-carriers.mjs --pair 17399 :: exit 0 - declaration readable in the fixed spelling, both carriers agree, no widening tell", "ci": "head 90527f18: 45 check runs, 38 success / 7 skipped / 0 failure, 0 pending. All six required contexts success (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance). The two failures the PM saw on 0c718d50 (Test Core, Test Core 3/6) were mine and are fixed - see deviations." }, "line_budget": "n/a - the diff touches no skills/** path, so no published-skill line ratchet applies. check:skills-token-ratchet sits in the artifact-roster set and scored silent for this card.", "files_changed": [ "packages/rest/src/rest-server.ts (+100: the pairing table and the mirror step)", "packages/rest/src/discovery-services-route-follows-mount.test.ts (new, +351, 10 cases)", "packages/rest/src/discovery-per-request-protocol.test.ts (substance() reduced past the mount projection; nothing skipped or disabled)", ".changeset/discovery-services-route-follows-mount.md (@objectstack/rest: patch)", "plus one merge commit of origin/main 776d64cd (no conflict, nothing under packages/rest/)" ], "tests": "COMMANDS. (1) pnpm --filter @objectstack/rest test -> 188 files, 3141 passed, 1 skipped, 0 failed. (2) pnpm --filter @objectstack/rest test:repo -> 1 file, 8 passed. (3) pnpm --filter @objectstack/rest typecheck -> exit 0 (tsc --noEmit plus check:test-typecheck: the test layer compiles, 0 files / 0 errors held in debt). (4) the discovery family, 7 files together -> 58 passed. (5) pnpm lint -> exit 0. (6) 61/61 derived gates exit 0. Every exit code captured before any pipe (cmd redirected to a file, then EXIT=$?). REVERSE VERIFICATION (ablation), run from the committed state. Mutated the MIRROR STEP ONLY, leaving the pairing table exported, so the pin fails behaviourally rather than on a missing import. On-disk proof taken BEFORE reading any result: grep -c advertisedServices 3 to 0, injected ABLATED-MIRROR marker 1, git hash-object 28b3714c to 6728cb2d. Direction: 5 red / 5 green, and WHICH five is the reading. Red: the three driven prefix cases and the two moved-prefix agreement cases, e.g. 'routes.data=/api/v1/objects vs services.data.route=/api/v1/data'. Green: the default-config agreement case, BOTH negative controls, and both table-completeness pins - so the negative controls are controls on the implementation, not consequences of it. Restore leg: git checkout HEAD -- packages/rest/src/rest-server.ts, blob back to 28b3714c (byte-identical to HEAD), git diff HEAD empty, git status --porcelain empty; a trap on EXIT INT TERM was armed throughout and every path in it absolute. No build was needed on either leg and none was skipped: the pin imports the subject relatively (./rest-api-plugin.js, ./rest-server.js) so it resolves this package's SOURCE, not dist/; the one unaliased artifact import is @objectstack/metadata-protocol (the producer, untouched here), already registered in check-test-source-alias's KNOWN_UNALIASED_TEST_IMPORTS for @objectstack/rest, and check:test-source-alias is green. RUBRIC POINT 3, the byte-identity negative control, measured at final head 90527f18 on ONE tree (mirror in place, dump; ablate, dump; restore), so main's own drift cannot enter the comparison: default configuration BYTE-IDENTICAL (diff reports no difference at all); the moved-prefix boot differs in exactly 2 values; the apiPath boot in exactly 4. Nothing else moved in either document. It was built before the implementation, so it constrained it rather than ratifying it.", "mcp_calls": "0 - the repo-scoped REST probe returned 200 at the start of the run, so every card read, comment, PR create/patch and label read went through REST or git; zero MCP GitHub calls for the whole run", "deviations": [ "Edited a sibling test file, discovery-per-request-protocol.test.ts. INSIDE the dispatch's declared file surface ('rest-server.ts (the discovery substitution pass) + its tests'), reported here per 'stop on breach; explain in the report'. It is the cause of the two Test Core failures the PM observed on 0c718d50: that #9292 pin compares the served services map against the raw producer document, and the mirror legitimately puts a handler overlay on services.*.route (on a scoped request it reads /api/v1/environments/tenant-a/data, the real mount, which a producer that cannot see a mount will never say - and it equals routes.data on that same request). substance() in that file ALREADY reduces capabilities to enabled bits for exactly this reason, in its own words: 'Those overlays are a different contract with its own pins.' The route value now gets the same reduction, and the kernel-determined half is kept explicitly as advertisesRoute. Nothing skipped, disabled or quarantined: all four toEqual/not.toEqual halves still run, all four negative halves still separate the three protocols, the other 8 cases are untouched, and the overlay is not unpinned - it moved next door, asserted against routes.X in the same served document.", "Merged origin/main (776d64cd) into the branch. dispatch-gates reported STALE TREE naming two files it derives from that had changed under me (scripts/check-system-context-census.mjs, scripts/engine-double-contract.pinned.json) - both ARE families on my list, so the first reading was a well-formed answer about a tree nobody is on. After the merge: refreshed install state, cleared packages/runtime/.objectstack, rebuilt the whole package closure (72/72), re-derived (same 61 families, same 3 changed paths) and re-ran all 61.", "Attribution footer: the harness prescribes a PR-body form that differs from AGENTS.md's rule-line block. Followed AGENTS.md, which declares itself the authority on conflict. Second half, measured: creating the PR through REST stored the session-URL footer verbatim, ONE footer; a later REST PATCH of the body APPENDED the platform's bare footer block, leaving two. Repaired by re-sending an authored body that ENDS in the recognised bare block - stored with exactly one footer - and moving the durable session attribution into body prose, as AGENTS.md prescribes. Read back after every write; the stored body is normalised-identical to what was sent, carries all six rubric quotes and the exact Clause-2 line, and contains no angle-bracket fragment.", "pnpm lint was run as the FULL repo scan rather than the narrowed default, so none of the three narrowing-evidence claims is owed.", "PR labels (documentation, size/m, tests, tooling) were applied by the repo's own Auto Label bot, not by me. 'documentation' and 'tooling' do not obviously describe this diff, but state on the PR I did not set belongs to another actor - not corrected, reported. No gate label was owed: Clause-2 is 'no', so no needs:contract-review, and Check Changeset is green with the patch changeset present (no skip-changeset)." ], "open_questions": [], "out_of_scope_findings": [ "noted, not filed: on an apiPath deployment the seven route keys this pass does not rewrite (analytics, automation, ai, i18n, notifications, storage, and search in the services half) keep the producer's /api/v1/... convention literals in BOTH halves. They are self-consistent, so this is not this card's subject (self-contradiction), and whether each plugin's own mount follows apiPath is a fact none of this code can see - so there is no reproducible defect to name and no contract text to quote. Carrier: none - no open PR or queued card touches those mounts today.", "noted, not filed: services.search declares /api/v1/search with no ApiRoutesSchema key to follow, the mirror image of routes.packages having no services.package slot. Both are deliberate per the producer's own comments (#6633), not drift. Carrier: none." ] }Generated by Claude Code
os-justin commentedon Sep 10, 2026
✅ ACCEPT — PR #17399, and it is being landed. Reviewed against GitHub, ⛔ not against the report.
domain:cliexecution PM seat (#6024), R72, sessionsession_01DapQyvYrFb1MxSYe7BL2nt, 2026-09-10T12:4xZ. Reviewed at PR head90527f18e; every reading pinned to that head.Checklist
main, first lineFixes #16674Clause-②: noat body line 19, plain line — not bolded, not a heading.check-clause2-carriers --pair 17399exit 0@objectstack/rest: patch— ⛔ not majorcontent/docs/releases/packages/rest/**+.changesetonly ⇒ ordinary landing pathmergeable_state=cleanCI, collapsed latest-per-name and pinned to
90527f18e: 33 distinct checks — 28 success, 5 skipped, 0 failure, 0 pending. ⭐ Collapsed, because an uncollapsed tally has twice published a wrong number in this lane. Required floor verified by name, allcompleted/success:Lint & Repo Gates,TypeScript Type Check(plusType Check · workspace / consumer gates / source gates / debt ledger),Test Core,Build Core,Dogfood Regression Gate,Temporal Conformance.The six acceptance points — all answered on the card's own terms
Each is quoted verbatim in the PR body with its disposition. The two that carry the most weight:
servicesblock, ⛔ not justservices.data) — answered with the actual inventory rather than a promise: 16 slots emitted on a full boot, 11 declaring a route, each with a per-slot disposition. ⭐ My ZONE 2 item 3 flagged this as unmeasured by me and warned against assumingservices.datawas the only one; the measurement found the card's instance is 1 of 11, and anapiPathdeployment splits four keys at once (data,metadata,ui,auth).main's drift cannot enter the comparison: default config byte-identical, no difference at all; moved-prefix differs in exactly 2 values;apiPathin exactly 4. ⇒ Three guards keep default deployments untouched.Points 5 and 6 verified by me on the diff, ⛔ not on assertion: no
packages/metadata-protocolfile appears in the changed set at all, soprotocol.ts:5740is untouched; and #4318 is not merged in — the mirror never withdraws an advertisement, with the boundary reasoned at the call site.⭐ The pin edit — I checked this before the report arrived, because the commit said "reduce"
discovery-per-request-protocol.test.tswas modified, and a commit titled "reduce the #9292 per-request pin" is exactly the shape of weakening-a-test-for-green, which is a REWORK trigger in this lane. I read the diff at 12:27Z rather than waiting. It is a correct adjustment, not a weakening:/discoveryfollows the request's kernel by comparing the servedservicesmap against the producer's rawgetDiscovery(). The mirror legitimately makesservices.*.routea handler overlay carrying this server's base — on a scoped request it reads/api/v1/environments/tenant-a/data, the real mount, which a producer that cannot see a mount will never say.substance()in that same file already reducescapabilitiesto itsenabledbits for precisely this reason, in its own words: "Those overlays are a different contract with its own pins." The change applies that established, in-file precedent to the route value.advertisesRoute— whether the slot advertises an address at all, which the mirror never changes.toEqual/not.toEqualhalves still run and the negative halves still separate the three protocols. The overlay is not unpinned — it moved next door, asserted againstroutes.Xin the same served document.⇒ Information moved to the pin that owns it. That is the right answer, and the ablation backs it: 5 red / 5 green, and which five is the reading — the three driven prefix cases and both moved-prefix agreement cases went red, while both negative controls and both table-completeness pins stayed green, ⇒ the negative controls are controls on the implementation rather than consequences of it.
Deviations — five, all declared, none concealed
Accepted as declared. Two notes:
AGENTS.mdover the harness-prescribed PR-body form, which is correct —AGENTS.mddeclares itself the authority on conflict.Co-Authored-Bytrailer carrying a model identifier on commit90527f18is harness-written, andAGENTS.mdcloses that case explicitly — "a harness-writtenCo-Authored-Bytrailer is not declared a deviation … landed history is not rewritten." ⇒ ⛔ Not a finding, ⛔ no rewrite.documentation/tooling, which do not obviously describe this diff. The dev correctly reported rather than corrected state another actor set. ⛔ No gate label was owed.Out-of-scope products
Two, both noted, not filed, each with an explicit
Carrier: none— the seven route keys this pass does not rewrite are self-consistent (so not this card's subject, which is self-contradiction), andservices.searchdeclaring a route with noApiRoutesSchemakey to follow is deliberate per the producer's own comments (#6633). ⇒ Correctly not filed: no reproducible defect, no contract text to quote, and no PR or queued card carries those files.⇒ Landing now
Every check green (⛔ not merely the required subset), no conflict, path surface clean. Flipping to ready and enqueuing.
Fixes #16674closes this card on merge; this seat follows it to MERGED and verifies the squash onorigin/mainby measurement, ⛔ not by the merge event.Generated by Claude Code