Skip to content

finding(spec): RestApiEndpointSchema.handlerStatus is authorable but has zero runtime consumers — the 501 it is documented to cause comes from somewhere else #13823

Closed

Description

@huangyiirene

Filed out-of-scope from the published-skills factual sweep, flight ⑦ (skills/objectstack-api, card #13814, program #13658). Recording only — unassigned.

The measurement

handlerStatus (implemented / stub / planned) is an authorable key on RestApiEndpointSchema (packages/spec/src/api/plugin-rest-api.zod.ts). A repo-wide search for the identifier, excluding skills/** and test files, returns three hits and all three are in the declaring file itself:

  • plugin-rest-api.zod.ts:177 — the declaration on RestApiEndpointSchema
  • plugin-rest-api.zod.ts:1415 — the same enum re-declared on RouteCoverageEntrySchema
  • plugin-rest-api.zod.ts:1431 — a docblock saying adapters SHOULD warn when handlerStatus !== 'implemented'

Plus one CHANGELOG row. No consumer in packages/runtime, packages/rest, or anywhere else reads the key.

Why it is worth a card rather than a shrug

The key's documented effect is a specific runtime behaviour, and that behaviour has a different cause. DispatcherErrorCode.enum.NOT_IMPLEMENTED is emitted from exactly four sites — runtime/src/endpoint-executor.ts (three) and runtime/src/api-mapping.ts (one) — and every one of them is the declarative-endpoint executor refusing a target or a mapping it cannot serve. None consults handlerStatus. So an author who sets handlerStatus: 'stub' expecting the dispatcher to answer 501 gets an ordinary served route, and the declaration reports progress to nobody: RouteCoverageReportSchema is the only shape that would carry it outward and nothing constructs one.

This is the ADR-0049 enforce-or-remove class, and the same shape as #13613: a declared surface whose semantics no code implements. It is also a declared = enforced gap on the endpoint vocabulary — the same surface ApiEndpointSchema closed strictly in #5384 precisely so a typo could not parse green and configure nothing.

Two routes, for triage

  • Remove. Retire the key from RestApiEndpointSchema with a tombstone, and either retire RouteCoverageReportSchema alongside it or keep it as the report-only vocabulary it already is. Cheapest, and it matches the startup-scope posture for a key with no pull.
  • Enforce. Have the route registrar consult handlerStatus and mount a 501 stub for stub / planned. That is a real capability — a declared-but-unbuilt route answering an honest 501 instead of 404 — but it is new behaviour needing its own ruling, not a repair.

Recommendation: remove, unless someone can name a consumer that wants the coverage report. The evidence for "no pull" is that the key has shipped since the CHANGELOG row and nothing has grown to read it.

Provenance

Found while verifying the skill sentence "The dispatcher returns 501 NOT_IMPLEMENTED for stub and planned handlers", which flight ⑦ corrects in the skill text. Fixing the skill stops the false teaching; it does not close the declared-but-inert key, which is why this is a separate card.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 31, 2026
  2. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:specDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 31, 2026
  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊定级 → domain:spec · needs-user-decision · p3 · tooling。摘 finding。

    锚定。 packages/spec/src/api/plugin-rest-api.zod.ts ⇒ domain:spec(唯一所有者)。

    ⭐ 与 #13612 / #13613 是同一个待裁问题的第三个实例 —— 一次裁决覆盖三张

    三张都是 ADR-0049 的 enforce-or-remove,都问同一句「绑定/强制,还是退役」:

    卡 声明面 爆炸半径
    #13612 六个 branded identifier(+3 个「绑定了但无读者」) ⚠️ 最大 —— bind 会改 object name / field name 的 accept 集(内联正则允许前导下划线,SnakeCaseIdentifierSchema 不允许)⇒ 存量元数据可能被拒
    #13613 EventNameSchema 的三个绑定 schema 零运行期消费者 中 —— bind 会收窄 WebSocketEventSchema.channel(今天是裸 z.string())
    本卡 RestApiEndpointSchema.handlerStatus 零消费者 ⭐ 最小 —— remove 只是一次带墓碑的键退役;enforce 是新能力(声明未建的路由回 501 而不是 404)

    ⇒ 建议一句方向覆盖三张(「本仓对 ADR-0049 的 declared-but-unenforced,默认走 retire 还是 bind」),各卡再按自己的半径评估落地代价。⛔ 三张不合并 —— 半径差一个数量级,合并会让最小的那张背上最大的那张的风险。

    本卡自身的读数,已复核口径

    标识符全仓搜索(排除 skills/** 与测试)三处命中,全在声明文件内::177(RestApiEndpointSchema 上的声明)· :1415(RouteCoverageEntrySchema 上重复声明的同一 enum)· :1431(一段说 adapter SHOULD 在 handlerStatus !== 'implemented' 时告警的 docblock),外加一行 CHANGELOG。⇒ packages/runtime、packages/rest 及其它任何地方零消费者。

    ⭐ 而它被文档化的效果,来自别处:DispatcherErrorCode.enum.NOT_IMPLEMENTED 恰好从四个站点发出(runtime/src/endpoint-executor.ts 三处 + runtime/src/api-mapping.ts 一处),每一处都是声明式端点执行器拒绝一个它服务不了的 target 或 mapping,没有一处查 handlerStatus。

    ⇒ 一个设了 handlerStatus: 'stub' 并期待 dispatcher 回 501 的作者,得到的是一条正常服务的路由;而那个声明向谁都没有报告进度 —— RouteCoverageReportSchema 是唯一会把它带出去的形状,而没有任何东西构造它。

    我认同「remove」的推荐,并补一条判据

    卡片说:remove,除非有人能指名一个想要 coverage report 的消费者。⇒ 这条判据是可判定的,应当在裁决前跑一次:除了 RouteCoverageReportSchema 之外,有没有任何 published skill / 文档 / 外部集成在教作者用 handlerStatus?

    ⚠️ 本卡的出处恰好说明这一点非零:它是在核对技能句子「The dispatcher returns 501 NOT_IMPLEMENTED for stub and planned handlers」时发现的 —— 也就是说,已发布的教材曾经在教这件不存在的事(flight ⑦ 正在改)。⇒ 若还有别处也在教,remove 就要带一次教材清扫,而不只是一次墓碑。

    定级 p3:零行为、零错误答案上线。不判更低,是因为它同时是一处 declared = enforced 缺口,而 ApiEndpointSchema 在 #5384 上恰恰严格关掉过同一类(让一个拼写错误无法解析成功却什么都不配置)—— 同一个端点词表上两种口径。

    Refs:#13814 / #13658(发现现场)· #13612 / #13613(同问题的另两个实例)· #5384(同词表上的严格化先例)· ADR-0049。


    Generated by Claude Code

  4. huangyiirene commented on Sep 1, 2026

    @huangyiirene
    CollaboratorAuthor

    裁决:remove —— handlerStatus 带墓碑退役;类方向同笔入册(维护者 2026-09-01,总监批 #27)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 维护者对本批逐字:「同意」。

    1. remove:RestApiEndpointSchema.handlerStatus 按 spec-property-retirement 手册退役(liveness 台账 verdict 确认 → ADR-0087 转换 → 墓碑 + 生成基线/表单/文档/pin 全套);RouteCoverageEntrySchema/RouteCoverageReportSchema 随同一 PR 按 liveness 读数处置(零构造者即同退);
    2. 前置一扫:退役前跑教材扫描 —— 除 flight ⑦ 已纠的那句外,还有哪些 published skill / 文档 / 示例在教 handlerStatus;非零则同笔清扫(教材曾教过不存在的 501 行为,是本卡的出处);
    3. enforce 排除:「声明未建的路由回 501」是零拉力的新能力,不是修复;
    4. 类方向入册(finding(spec): all six branded identifier schemas in branded-types.zod.ts are declared but unbound — no schema anywhere composes them #13612 / finding(spec): EventNameSchema's only three binding schemas have no runtime consumer — the live event vocabulary is a closed enum that never touches it #13613 引用用):ADR-0049 declared-but-unenforced 的默认处置 = retire(零拉力键);enforce/bind 只当有具名消费者或实测拉力。⛔ 本裁决不直接裁那两张 —— 半径差一个数量级,各自车道按此方向 + 自身半径(accept-set 位移读数)评估,拿不准回箱;
    5. 条款②:YES(发布 accept 面收窄)⇒ draft + needs:contract-review 同笔,本席复审;changeset 按退役惯例。

    状态转移(同笔)

    needs-user-decision → pm:queue;tooling priority:p3 domain:spec 不动。


    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Sep 1, 2026
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 2, 2026
  7. claude commented on Sep 2, 2026

    @claude
    Contributor

    Claim: PM loop, domain:spec seat (seat post #6017)
    Session: session_01GDA48PuRFrHyRfdkBz8m21
    Branch: claude/issue-13823-retire-handler-status
    Worktree: objectstack-issue-13823
    Domain: domain:spec
    File surface: packages/spec/src/api/plugin-rest-api.zod.ts (handlerStatus on RestApiEndpointSchema retired with a tombstone; RouteCoverageEntrySchema / RouteCoverageReportSchema retired alongside on the liveness reading — zero constructors) + the retirement playbook's full set: liveness ledger verdict, ADR-0087 conversion entry (tail-append under packages/spec/src/migrations/entries/ + regenerated registry.ts), generated baselines (api-surface / authorable-surface / export-origins / json-schema / declaration-map), forms if any offer the key, reference docs, pin tests, changeset; plus the teaching sweep's hits in content/docs/** and examples/** (published skills/** hits are reported, not edited — governed surface, skills lane). ⛔ No enforce path (ruled out).
    Container & model: M, mode:subagent, served claude-fable-5-1 (Clause-② yes: published accept set narrows; fuse literal reads claude-fable-5, posture on #14303).
    Clause-②: yes (declared on both carriers at PR time).
    Ruling implemented: director batch #27, maintainer 2026-09-01 verbatim 「同意」 (comment 5494755488) — remove; teaching sweep first; enforce excluded; class direction registered for #13612 / #13613 (not ruled here).
    Serial constraints: no in-flight writer of plugin-rest-api.zod.ts; ADR-0087 registry.ts tail-append and merge=os-regen products relay against the other in-flight spec PRs (#13801 phase 2, #13809 — describe-only, no registry entries) — the later pusher resyncs via scripts/pm/os-regen-merge.sh.


    Generated by Claude Code

  8. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 2, 2026
  9. claude commented on Sep 2, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 13823,
      "status": "done",
      "branch": "claude/issue-13823-retire-handler-status",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14531",
      "head": "875e3fa5e",
      "premise_still_valid": true,
      "summary": "Retired RestApiEndpointSchema.handlerStatus per the spec-property-retirement playbook, exactly as ruled (remove; enforce excluded): retiredKey() tombstone on the non-strict RestApiEndpointSchema (prescription cites ADR-0049; the issue id lives in the adjacent comment after check:doc-authoring refused it in customer-facing text), whole-def removal of RouteCoverageEntrySchema / RouteCoverageReportSchema (re-measured: zero constructors in objectstack, objectui @d8ec8d6d, cloud) and of the now-orphan HandlerStatusSchema enum; ADR-0087 entries tail-appended (retired-keys 18.api__RestApiEndpoint__handlerStatus, retired-defs 18.api__HandlerStatus / RouteCoverageEntry / RouteCoverageReport, semantic 18.rest-api-endpoint-handler-status-retired) with registry.ts regenerated; deliberately no D2 conversion (nothing parses RestApiEndpointSchema outside its own unit tests; no stack seam) and no os migrate meta sentence. Premise re-measured on origin/main a9b2be0b0: the three declaring-file hits are unchanged; NOT_IMPLEMENTED emitters are endpoint-executor.ts x3, api-mapping.ts x1 plus one the card did not count (api-endpoint-step.ts:343), none reading the key. Liveness ledger: no row moves because RestApiEndpointSchema is outside the walked population (not a registered metadata type, not in SPEC_ONLY_SCHEMAS) - adding one would be an ORPHAN; the dead verdict is recorded in the schema, the D3 entry and the changeset. Generated baselines: json-schema.manifest -3 keys and authorable-surface -10 rows deleted deliberately (the #4725 manifest deletion gate and check (c) adjudicated them), handlerStatus row now [RETIRED], api-surface / export-origins / declaration-map -6 names, reference docs + strictness-ledger counts regenerated; spec-changes.json and the upgrade guide are unchanged because they project majors up to 17 only (same as every 18-registered precedent). Pin tests added (rejection with code+path+prescription for all three former values incl. the prose-only default 'implemented', the route-registration embed path, positive control, shipped default registrations, tsc channel, zero holders for six names, registry membership); type-alias pin count rebalanced 835->832. Teaching sweep: no hand-written docs/examples/READMEs/create-objectstack hit; generated reference regenerated; skills/objectstack-api/SKILL.md taught the key at the base (four sites, not edited - governed surface) but main already cut every one in #14448 (bae4088f3): zero hits under skills/ at 99b8faf8f with RestApiEndpoint still hitting as the live control - none on main, no skills-lane follow-up; the changeset says so. Relay: 12 commits landed on main since BASE; the only moved os-regen paths are 14 content/docs/references pages my diff does not touch, so no merge was needed. Clause-two: needs:contract-review added to PR #14531 and the card with matching comparative read-backs; PR left draft. Two CI/coordinator corrections applied as plain commits after the draft opened: the pin-count assertion literal (7cbf0ed60) and the changeset's skills sentence (875e3fa5e).",
      "tests": "Every heavy step under scripts/pm/os-verify-lock.sh; exits captured before any pipe; verdict lines quoted. FINAL HEAD 875e3fa5e: spec build exit 0 (gen:schema: '3 schema(s) left the published set since a9b2be0b02a3, each declared (#4725)'; '2 baseline deletion(s) ... carry their own proof (#4650)'; check-dts-emitted 34/34); check:generated exit 0 ('All 15 generated artifacts are up to date'); targeted vitest (exec vitest run --maxWorkers=2 type-alias-convention.pin + plugin-rest-api.handler-status-retirement + plugin-rest-api + retired-key-migrate-sentence + migrations): 'Test Files 5 passed (5)', 'Tests 172 passed (172)'; check:skill-examples exit 0 ('265 prose examples type-check across 3 surface(s)', client-react closure built); check:adr-0087-registration exit 0 ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'); check:empty-changeset, check-changeset-no-major, check:nul-bytes, check:doc-authoring, check:spec-parsed-alias exit 0; git status clean after the build. 7cbf0ed60: targeted vitest 5/172 passed; check:test-typecheck 0; spec-parsed-alias/doc-authoring/nul-bytes 0; check:api-surface refused (dist older than src after the test edit - not a measurement; settled by the rebuild). 90d567cd3 (full union): spec typecheck exit 0 (tsc + scripts + 'check:test-typecheck: OK - 54 file(s) / 262 error(s) / 146 pinned signature(s)', ledger unchanged; new test file proven in the tsconfig.test.json program via --listFiles); FULL spec vitest --maxWorkers=2: 'Test Files 1 failed | 451 passed (452)', 'Tests 1 failed | 12190 passed (12191)' - the one failure was the pin-count toHaveLength literal (835), fixed in 7cbf0ed60 and green in chains 3 and 4; check:skill-examples 0; check:exported-any / dual-source-exports / entry-nameability / browser-reachable-entries 0; check:generated 0 (15/15); doc-authoring / nul-bytes / adr-0087 / spec-parsed-alias 0; check-dev-prereqs exit 1 = 'The workspace is not built - 32 of 67 workspace packages declare an entry point under dist/ that is not on disk' (pnpm dev precondition, unrelated - NOT MEASURED). Also exit 0 on the same tree earlier: check:variant-docs, empty-state, llms-txt, yaml-examples, template-manifests, error-code-provenance, objectui-pin-citations, quick-reference-counts, corpus-claim-drift, skill-identifier-liveness, doc-anchors, docs-single-h1, docs-audit-scope, test-source-alias, cross-package-test-inputs, published-files, type-check-coverage, migration-registry + liveness (inside check:generated), and root check-doc-frontmatter / docs-section-name / doc-route-spelling / section-landing-index / keyed-text-bounds / ci-filter-parity / comment-mask-adoption / shard-attestation / system-context-census / undeclared-dep-imports / plugin-teardown-shape / docs-audit affected-docs + drift-comment / pm half-states. Earlier reds fixed before landing: check:doc-authoring (issue id in the tombstone string) and the pin-count literal. NOT MEASURED (CI-owned): check:type-check-debt / --re-measure (exit 3 PREREQUISITE NOT MET, whole closure not built), check-test-completeness (grades CI's turbo test log), check:react-declaration-parity (objectui manifest), repo-wide pnpm lint. DECLARED NARROWING: the full 452-file suite ran on 90d567cd3; the two later commits touch one test literal and changeset prose and were re-verified with the targeted files, not the whole suite - CI's Test Core covers it on the final head. Gate list from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 74 commands at b86231707). CI on 875e3fa5e at report time: 31 check-runs — completed/skipped: 2, completed/success: 14, in_progress/None: 15.",
      "mcp_calls": "0 - every GitHub read/write went through authenticated REST (curl) and local git; no MCP GitHub tool was called",
      "open_questions": [],
      "out_of_scope_findings": [
        "skills/** teaching hits: none on main (measured at 99b8faf8f - git grep -n -i -E 'handlerStatus|handler status|RouteCoverage|HandlerStatus' origin/main -- skills/ returns zero; reverse control RestApiEndpoint still hits skills/objectstack-api/SKILL.md). At this PR's base a9b2be0b0 the skill carried four sites (SKILL.md:136 handlerStatus: 'implemented' in the os:check-marked RestApiEndpointSchema.parse example; :380 the '501 NOT_IMPLEMENTED - Route declared but handler is a stub' row; :383-391 the Handler Status section; :393-395 flight 7's #13808 correction) - all cut by #14448 (bae4088f3) before this report; nothing to card.",
        "OBSERVATION, no card needed: the card's NOT_IMPLEMENTED emitter count (four) misses packages/runtime/src/api-endpoint-step.ts:343 - a fifth emitter of the same class (executor refusing what it cannot serve), none consulting handlerStatus; recorded in the PR body and the D3 entry."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  10. claude commented on Sep 2, 2026

    @claude
    Contributor

    ACCEPT — in-seat Clause-② contract review PASS on PR #14531 (head 875e3fa5). domain:spec seat, session session_01GDA48PuRFrHyRfdkBz8m21, seat post #6017; served model string claude-fable-5-1 (floor reading per the #14303 record, open to maintainer veto).

    Report: 5508368645 (status: done; full spec suite 452 files / 12190 passed on 90d567cd, the two later commits re-verified with targeted files and declared as such; gates derived, exits captured; NOT MEASURED items are CI-owned and CI is running them on the final head).

    Review against the director ruling (batch #27 「同意」: remove, enforce excluded) and the spec-property-retirement playbook:

    • Tombstone, not deletion. RestApiEndpointSchema is a non-strict z.object, so handlerStatus: retiredKey(HANDLER_STATUS_RETIRED) is the correct shape (bare deletion = silent strip, ADR-0104). Prescription names the key, the removal, the real 501 mechanism (declarative-endpoint executor), the action (delete the key) and ADR-0049; no issue id in customer-facing text (check:doc-authoring).
    • Whole-def removals justified. RouteCoverageEntrySchema / RouteCoverageReportSchema had zero constructors (route 3) and HandlerStatusSchema is an orphan value enum once both carriers leave (refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up) #3950). Seat census at objectui 63b4e0e and cloud 061e83c: zero holders of all six names, grep validated by a live control.
    • ADR-0087 registration complete and generated, not typed. Entry files retired-keys/18.api__RestApiEndpoint__handlerStatus, retired-defs/18.api__{HandlerStatus,RouteCoverageEntry,RouteCoverageReport}, semantic/18.rest-api-endpoint-handler-status-retired; registry.ts diff lands only inside the three generated regions. No D2 conversion by design (no seam ever parses the schema) and the pin asserts that absence. Registered under 18 while shipping on the 17.x line, with the tombstone worded "removed in @objectstack/spec 17" — matches the dominant 18-registered precedent (context.previewMode, manifest.capabilities, measures.<metric>.filters).
    • Pins assert the strongest set the surface has. invalid_type at ['handlerStatus'] for all three former values including the prose-only default, the nested path through RestApiRouteRegistrationSchema, the no-strip positive control, the shipped default registrations, the tsc never channel, zero holders on every public entry, registry membership under 18; type-alias pin count rebalanced 835 → 832 with its receipt.
    • Generated products consistent. api-surface / export-origins / declaration-map −6 names, authorable-surface [RETIRED] row + −10 rows of the two removed defs, json-schema manifest −3 (adjudicated by the json-schema.manifest.json 的「deliberate removal」删行仍是纪律而非门禁 —— #4650 的同类洞,上移一层(整 schema 级) #4725 gate), strictness ledger api/ 455 → 452, reference pages 1593 → 1590. Liveness ledger correctly untouched (schema outside the walked population).
    • Changeset. @objectstack/spec minor, BREAKING, <!-- adr-0087: registered rest-api-endpoint-handler-status-retired -->, FROM → TO rows, one-line fix; the teaching-sweep sentence now states what main carries (the skill's four sites were cut by skills(api): optimization flight — RestApiEndpointSchema example and other packages' anchors cut, the apiMethods rule stated once, http.server mount / error ledger / OCC taught (net −1,676 tokens) #14448 / bae4088f3 before this PR) — no skills-lane card is needed; the seat re-measured: zero skills/** hits at c1f7e9f6 and 99b8faf8.
    • Two CI corrections landed as plain commits, both this PR's own: the pin-count literal (7cbf0ed6) and the changeset sentence (875e3fa5); neither widened the diff.
    • Pre-flip reads at 875e3fa5: closing keyword on the PR body = Fixes #13823 only; check-governed-merges --test on the 18-file list: NOT governed; no content/docs/releases/ edit; trial merge CLEAN vs origin/main fa7292c4; zero base drift on the PR's files. (Re-run once more at the flip.)

    Observation from the report, recorded and not carded: a fifth NOT_IMPLEMENTED emitter (packages/runtime/src/api-endpoint-step.ts:343) of the same executor class, also never consulting the key — the retirement record already names it.

    Gate: needs:contract-review cleared on both carriers by this comment's transaction (read-modify-write, compared read-back). Landing: ready + auto-merge only once every check on head 875e3fa5 is green (CI restarted 10:41Z); the queue squashes.


    Generated by Claude Code

  11. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 2, 2026
  12. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpriority:p3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions