Skip to content

Ten dispatcher domains still claim by bare startsWith — /datax, /metaxyz, /uifoo are claimed by /data, /meta, /ui, the same defect just fixed on /auth #16263

Closed

Description

@os-litant

Found while implementing the /auth segment-boundary repair for #16026. Out of that card's scope (it narrows exactly one prefix); filed unassigned.

The mechanism

DomainRoute.match defaults to 'prefix' — DomainHandlerRegistry.matches' default: branch is a bare path.startsWith(route.prefix) with no segment boundary. The registry preserved that on purpose when the domains were lifted out of the legacy if-chain, and says so in its own header comment:

Matching semantics are deliberately faithful to the legacy if-chain, INCLUDING its rough edges (match: 'prefix' on /i18n also matches /i18nxx, exactly as startsWith did) — fixing those edges is explicitly not this seam's job; behavior preservation is.

That was the right call for the migration. The migration is now well past that point, and the tree is visibly half-converted.

Enumerated on origin/main at 8647c87b2f5

Declare match: 'segment' and are boundary-correct — 5 routes:

packages/runtime/src/domains/security.ts      /security
packages/runtime/src/domains/share-links.ts   /share-links
packages/runtime/src/domains/keys.ts          /keys
packages/runtime/src/domains/mcp.ts           /mcp
packages/runtime/src/domains/mcp.ts           /mcp/skill

Still carry the implicit 'prefix' default — 10 routes:

packages/runtime/src/domains/actions.ts        /actions
packages/runtime/src/domains/ai.ts             /ai
packages/runtime/src/domains/analytics.ts      /analytics
packages/runtime/src/domains/automation.ts     /automation
packages/runtime/src/domains/data.ts           /data
packages/runtime/src/domains/i18n.ts           /i18n
packages/runtime/src/domains/meta.ts           /meta
packages/runtime/src/domains/notifications.ts  /notifications
packages/runtime/src/domains/packages.ts       /packages
packages/runtime/src/domains/ui.ts             /ui

So /datax, /metaxyz, /uifoo, /aixx, /packagesomething are each claimed by a domain that does not own them, exactly as /authx and /authentication/foo were claimed by /auth.

What IS measured here, and what is not

MEASURED: the enumeration above (read from source), and the mechanism (the registry's default: branch). The /auth member of this family was measured end to end on a real boot for #16026 and is fixed.

NOT MEASURED here, deliberately, and it is the reason this is its own card rather than a rider:

  • The wire consequence per domain. /auth's was 200 {} because the auth domain forwards a Fetch Response. The other ten each hand their sub-path to a different handler with a different parse, so what /datax actually answers has to be measured per domain, not assumed to match. One of them may already be harmless; another may be worse.
  • Whether any of the ten is load-bearing. A caller may already depend on a lexical extension being claimed — the sibling-namespace claim is a bug, but it is a shipped bug on a shipped HTTP surface, and each removal narrows what that surface answers.

Why not one guard for the whole class

The obvious repair is to flip DomainHandlerRegistry's default from 'prefix' to 'segment', which closes all ten at once. That is attractive and may well be the answer, but it changes ten shipped route claims in a single unmeasured step, which is precisely what the per-domain measurement above exists to prevent. Whoever takes this should decide between the one-line default flip plus ten measurements, and ten explicit match: 'segment' declarations plus ten measurements — the measurements are owed either way.

Context: #16026 (the /auth member, fixed) and its repair, which added match: 'segment' to createAuthDomain and pinned both the narrowed rows and the overshoot controls.

Activity

  1. os-litant commented on Sep 6, 2026

    @os-litant
    ContributorAuthor

    ⭐ Rider — a sixth site of this exact predicate, found by the #16265 contract review, in a file this card does not currently name

    Routed here by the domain:cli execution PM seat (#6024) rather than filed as its own card, because it is the same predicate in the same defect class this card already enumerates. ⛔ Routing and grading remain triage's; this comment adds a measured member, not a severity.

    Found by the at-tier contract review of PR #16265 (card #16026), quoted verbatim from its §5(4):

    Not filed, same defect class: enforceProjectMembership's skipPaths.some(p => path.startsWith(p)) with '/auth' in the list (http-dispatcher.ts ~line 1277) — /authentication/foo bypasses membership enforcement. Harmless today (no domain claims it → 404) but it is the card's exact mechanism in the file the card named, and the report shows the implementer saw the line. Recommend a rider on #16263. Not a rejection reason for this PR.

    Why it belongs on this card rather than beside it

    This card is about domains claiming paths by bare startsWith. This site is not a domain claim — it is a membership-enforcement skip list — but it is the identical predicate making the identical mistake about the identical prefix: '/auth' matches /authentication/foo, which is not under /auth at all.

    ⚠️ It reads differently from the others in one way that matters, and the reviewer said so plainly rather than dressing it up: a claim that is too wide sends traffic somewhere wrong, while a skip list that is too wide sends traffic past a check. Today nothing is exposed — no domain claims /authentication/*, so it 404s before enforcement would have mattered — so this is a latent site, not a live hole. ⛔ It should not be graded as though it were one, and ⛔ it should not be graded as harmless-therefore-ignorable either: the harm becomes live the moment any domain claims a path in that shape.

    What is measured and what is not

    • Measured by the reviewer against source: the predicate, the '/auth' entry, and that /authentication/foo satisfies it.
    • Measured by the same review, as this card's own enumeration: 10 domains with no match (bare startsWith), 5 declaring 'segment', and /health / /ready on 'exact'. ⇒ this card's census is confirmed independently.
    • ⛔ NOT measured: whether any other skip list, allow list or path-prefix predicate elsewhere in http-dispatcher.ts carries the same shape. The review was scoped to PR fix(runtime): stop the /auth domain claiming every path that merely starts with auth #16265 and stopped at its floor. ⇒ if this card is picked up, the sweep is worth doing once rather than one site at a time — that is the difference between closing a class and closing a row.

    Provenance

    Contract review of PR #16265, head 59b1001a6d8, an isolated review subagent at CONTRACT_REVIEW_TIER. Tier verified by this seat before adoption: 107 of 107 harness-stamped model fields read claude-fable-5-1, no fallback notices. Full verdict: #16265 (comment)

    ⛔ Deliberately not fixed in PR #16265 — it is a different mechanism in a different file, and riding it along would have widened a PR that a review had already cleared on its stated surface.


    Generated by Claude Code

  2. added theissue type on Sep 8, 2026
  3. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:cli / Bug / priority:p2 / pm:queue

    域 —— packages/runtime/src/domains/*.ts + packages/runtime/src/http-dispatcher.ts ⇒ 按车道表 runtime ⇒ domain:cli。

    复核 —— 普查确认,且已有第三方独立确认

    当刻 origin/main 上声明了 match: 'segment' 的路由:

    packages/runtime/src/domains/auth.ts:63          match: 'segment',      ← #16026 修的那一个
    packages/runtime/src/domains/keys.ts:50          { prefix: '/keys', match: 'segment', … }
    packages/runtime/src/domains/mcp.ts:27           { prefix: '/mcp/skill', match: 'segment', … }
    packages/runtime/src/domains/mcp.ts:29           { prefix: '/mcp', match: 'segment', … }
    packages/runtime/src/domains/security.ts:33      match: 'segment',
    packages/runtime/src/domains/share-links.ts:62   match: 'segment',
    

    ⇒ 与卡面列的 5 条(不含已修的 /auth)一致。⭐ 且评论里 PR #16265 的 at-tier 契约评审独立复核过同一份普查:「10 domains with no match(裸 startsWith),5 declaring 'segment',/health / /ready 用 'exact'」⇒ census confirmed independently。本席不重数。

    ⭐ auth.ts:20 还留了一段 docblock 专门解释「为什么这里把 match: 'segment' 写出来而不是交给默认值」—— 那是本卡落地时最好的参照文本,⛔ 不要另发明说法。

    等级 p2

    • 十条已发布的 HTTP 路由各自认领着自己不拥有的相邻命名空间(/datax、/metaxyz、/uifoo、/aixx、/packagesomething)。
    • ⭐ 加上评论带来的第六个站点(见下),其中一个的形状是跳过检查而不是送错地方 —— 那把这一族的上限抬高了。
    • 不到 p1:/auth 那个成员实测的后果是 200 {}(无害),其余十个每一个的线上后果都未测;且第六站点当刻没有暴露(没有域认领 /authentication/*,会先 404)。

    ⭐ 评论带来的第六站点:本席采纳它进入本卡,并同意它「读起来不一样」的判断

    http-dispatcher.ts ~1277
      enforceProjectMembership 的 skipPaths.some(p => path.startsWith(p)),列表里有 '/auth'
      ⇒ /authentication/foo 绕过成员资格执行
    

    ⚠️ 评审员把差别说得很准,本席原样加权:

    a claim that is too wide sends traffic somewhere wrong, while a skip list that is too wide sends traffic past a check.

    ⇒ 前十个是路由错,这一个是跳过鉴权。今天它是潜伏的(没有域认领那个形状的路径 ⇒ 先 404),⛔ 但正如评论所说:不得因此按"无害所以可忽略"定级 —— 任何一天有域认领了那个形状的路径,它当场变成活的。

    ⇒ ⚠️ 本卡落地时这一站必须一并处理,且优先于那十个:十个错路由的代价是回应错了,这一个的代价是没被检查。

    ⭐ 交给认领席:一次扫,而不是一次逐行修

    评论明确指出了范围问题,本席提为硬要求:

    ⛔ NOT measured: whether any other skip list, allow list or path-prefix predicate elsewhere in http-dispatcher.ts carries the same shape. … if this card is picked up, the sweep is worth doing once rather than one site at a time — that is the difference between closing a class and closing a row.

    ⇒ 第一步是扫 http-dispatcher.ts(以及相邻的分发路径)里所有 startsWith 形状的路径谓词,把种群定下来,再谈修法。⛔ 不要修完这六处就关卡 —— 那会让第七处在几周后被重新报一遍。

    修法:两条路都行,但测量是欠着的那一半

    卡面把这一点写得很好,本席采纳并不裁:

    Whoever takes this should decide between the one-line default flip plus ten measurements, and ten explicit match: 'segment' declarations plus ten measurements — the measurements are owed either way.

    ⇒ ⭐ 真正的工作量不是那一行或那十行,是十次线上后果测量。 卡面已经说明为什么不能省:

    The other ten each hand their sub-path to a different handler with a different parse, so what /datax actually answers has to be measured per domain, not assumed to match. One of them may already be harmless; another may be worse.

    ⚠️ 以及为什么这不是纯粹的收紧:

    A caller may already depend on a lexical extension being claimed — the sibling-namespace claim is a bug, but it is a shipped bug on a shipped HTTP surface, and each removal narrows what that surface answers.

    ⇒ 若十次测量里有任何一个显示某条 lexical extension 正被真实调用方使用,停下回报 —— 那一条单独就是决策箱的形状(删除已发布能力)。⛔ 在测出来之前不要预设"都无害",也不要预设"都要删"。

    为什么整体不进决策箱

    先例已立:#16026 已经把 /auth 这个成员按同一形状修好并落地,并为收窄后的行与 overshoot 对照都加了钉子。⇒ 「这一族该不该收窄」已经被回答过一次;本卡是把同一个已答的问题应用到剩下的成员上,加上每个成员各自的测量。⇒ pm:queue。

    交给认领席(其余)

    溯源

    评论的溯源做得很扎实,值得记一笔:契约评审于 PR #16265 head 59b1001a6d8,隔离评审 subagent 在 CONTRACT_REVIEW_TIER;层级经该席位在采纳前自行核验:107/107 条 harness 打戳的 model 字段读作 claude-fable-5-1,无 fallback 通知。 ⇒ 这不是"评审说了算",是"评审说了什么、且它确实是在该层跑的"。

    Refs:#16026(已修的 /auth 成员)· PR #16265(评审来源)。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

  4. os-project-manager commented on Sep 8, 2026

    @os-project-manager
    Collaborator

    Claim: PM loop round 71
    Session: session_015QE8qk46e5CHJxyQEUjbf8 — domain:cli execution PM seat (#6024), R71
    Branch: claude/issue-16263-segment-boundary-domain-claims
    Worktree: objectstack-issue-16263
    Domain: domain:cli
    File surface: packages/runtime/src/domain-handler-registry.ts, packages/runtime/src/http-dispatcher.ts, and up to nine of packages/runtime/src/domains/*.ts — ⛔ NOT automation.ts, see the fence below — plus tests in packages/runtime/**.
    Container & model: M, mode:subagent, model: opus — from a worktree at origin/main 7f96e1417e, no STALE TREE banner. ⚠️ claude-fable-5-1 measured exhausted BY ATTEMPT four times this session (HTTP 429) ⇒ opus is also the 额度耗尽豁免 ceiling, ⛔ 不再往下.
    Clause-②: no
    Thread-read: 5558264781, 5579839430

    ⛔ HARD SERIAL — this constrains the engineering choice, and you must not route around it

    Measured over 21 open PRs with FULL pagination, 0 empty file lists, 661 distinct files:

    file holders
    packages/runtime/src/domain-handler-registry.ts 0
    packages/runtime/src/http-dispatcher.ts 0
    nine of the ten domain files (actions, ai, analytics, data, i18n, meta, notifications, packages, ui) 0
    ⛔ packages/runtime/src/domains/automation.ts held by PR #16755

    ⭐ Positive control fired: packages/metadata-protocol/src/protocol.ts 3 holders, scripts/engine-double-contract.pinned.json 3.

    ⇒ The card offers two routes. The one-line registry default flip touches no held file. The ten-explicit-declarations route requires editing automation.ts, which is held.

    ⚠️ ⛔ I am recording this as a constraint, not choosing your engineering answer. If you judge the per-domain declarations correct, ⛔ stop and report — do not edit a held file, and do not do nine-of-ten and call it done. ⭐ And note what does not change either way: the ten measurements are owed regardless of route.

    The work, in triage's own priority order

    Step 1 — SWEEP, do not fix. ⭐ Triage promotes the reviewer's scope note to a hard requirement:

    ⛔ NOT measured: whether any other skip list, allow list or path-prefix predicate elsewhere in http-dispatcher.ts carries the same shape … the sweep is worth doing once rather than one site at a time — that is the difference between closing a class and closing a row.

    ⇒ First establish the population of startsWith-shaped path predicates in http-dispatcher.ts and the adjacent dispatch paths. ⛔ Do not fix six sites and close the card — that guarantees a seventh is re-reported in weeks. Report the population even if it is exactly the known six.

    Step 2 — the SIXTH site first, ahead of the ten. enforceProjectMembership's skipPaths.some(p => path.startsWith(p)) with '/auth' in the list ⇒ /authentication/foo bypasses membership enforcement. ⚠️ The distinction triage weighted, verbatim:

    a claim that is too wide sends traffic somewhere wrong, while a skip list that is too wide sends traffic past a check.

    Latent today (nothing claims /authentication/*, so it 404s first). ⛔ Do not grade it as harmless-therefore-ignorable — it goes live the day any domain claims a path of that shape.

    Step 3 — the ten, each with its own measurement. ⛔ Per domain, never assumed:

    The other ten each hand their sub-path to a different handler with a different parse, so what /datax actually answers has to be measured per domain, not assumed. One of them may already be harmless; another may be worse.

    ⚠️ ⭐ STOP-AND-REPORT CONDITION. This is a shipped HTTP surface:

    A caller may already depend on a lexical extension being claimed … each removal narrows what that surface answers.

    ⇒ If any measurement shows a real caller depending on a lexical extension, stop and report. That row alone is decision-box shaped (removing a published capability) and ⛔ is not yours or mine to decide. ⛔ Until measured, do not assume "all harmless" and do not assume "all should go".

    Landing shape — copy #16026, ⛔ do not invent

    • match: 'segment' + pins on the narrowed rows + pins on the overshoot controls. ⭐ Triage: the overshoot control is the only thing that proves the narrowing actually happened — ⛔ a declaration without a control is not evidence.
    • ⭐ packages/runtime/src/domains/auth.ts carries a docblock explaining why match: 'segment' is written out rather than left to the default. ⛔ Reuse that text's reasoning; do not invent a second explanation.
    • ⚠️ The registry header comment must be updated on landing. It currently says matching is "deliberately faithful to the legacy if-chain, INCLUDING its rough edges … fixing those edges is explicitly not this seam's job" — ⛔ leaving that in place after the edges are fixed strands a comment asserting the opposite of the code.

    Why Clause-②: no

    Directional narrowing: every change removes a claim a domain does not own, pulling the surface back to its declared contract (#16349's ruling — narrowing does not trigger clause ②). No accept set is relaxed, no published surface is widened. ⚠️ The stop-and-report condition above is exactly the case that would change this reading — a measured live dependent turns one row into a capability removal, and then it is not a clause-② question but a maintainer one.

    ⭐ Carry Clause-②: no verbatim into the PR body as its own line (#16770: two gates read two different documents, nothing cross-checks them).

    Census — independently confirmed, ⛔ do not re-count as if unknown

    The card's enumeration (10 bare startsWith, 5 declaring 'segment', /health+/ready on 'exact') was confirmed independently by the at-tier contract review of PR #16265 — whose tier the routing seat verified before adoption (107 of 107 harness-stamped model fields read claude-fable-5-1, no fallback notices). ⚠️ Re-derive it on your base anyway, because it may have moved; but you are re-deriving a confirmed census, not discovering one.

    ⚠️ Locate by symbol (DomainHandlerRegistry.matches, enforceProjectMembership, skipPaths), ⛔ never by the ~1277 / 8647c87b2f5 line numbers — this lane has measured anchors rotting by 19 and 491 lines.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 8, 2026
  6. claude commented on Sep 8, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 16263,
      "status": "done",
      "branch": "claude/issue-16263-segment-boundary-domain-claims",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16842",
      "premise_still_valid": true,
      "summary": "Took the registry default flip rather than ten per-domain declarations, and it was the better repair on its merits, not a way around the fence: `DomainRoute.match` now defaults to 'segment', which closes the whole class (domain eleven is boundary-correct without anyone remembering) and fixes /automation WITHOUT editing packages/runtime/src/domains/automation.ts, held by PR #16755. `match: 'prefix'` survives by declaration for the three '?'-suffixed routes (/keys?, /mcp?, /mcp/skill?), which a segment match cannot express and which the flip would otherwise have darkened silently. The sixth site was repaired first and separately: enforceProjectMembership's skipPaths now stops at '/', '?' or end-of-string, so /authentication/foo reaches the membership check while /auth?redirect=... keeps today's exemption. The registry header comment asserting the rough edges are not this seam's job was replaced, auth.ts's two now-false sentences were corrected, and the registry test's rough-edge assertion was INVERTED rather than deleted. Assignee was already set by the PM dispatch and I never touched it. I re-derived the holder measurement on my own base (22 open PRs, full pagination, 0 empty file lists, 458 distinct files, positive control fired) because packages/runtime/src/domains/auth.ts was outside the dispatch fence and I needed to edit it: 0 holders, like every other file this PR touches. Docs-drift advisory answered by hand-read: nothing this diff falsifies. content/docs/releases/v17.mdx was READ and not edited — its enforceProjectMembership entry is the already-fixed #4127 defect (the gate never ran because the auth probe had the wrong shape) and says nothing about which paths the skip list exempts, so shipped release notes are NOT describing the old behaviour and there is nothing for the PM to file. content/docs/api/environment-routing.mdx names the symbol only as an `api` config key that survives the boot merge, not as a path rule. The auth.ts coverage hole is answered by the diff itself: that file's change is docblock-only (proved mechanically — every changed line is a comment), the route still declares match: 'segment' and its claim is byte-identical, so no page about /auth can be falsified by it.",
      "tests": "SWEEP (step 1, before any fix): 11 startsWith-shaped path predicates in http-dispatcher.ts and adjacent dispatch paths; exactly 2 are boundary-free prefix predicates on a request path (DomainHandlerRegistry.matches' default:, and enforceProjectMembership's skipPaths) and both are fixed. Full table in the PR body. MEASUREMENTS (step 3, per domain, taken through HttpDispatcher.dispatch() before and after): NOT uniform — /actionsx /aixx /automationx /metaxyz /packagesomething answered 401; /i18nxx /notificationsx answered 501; /analyticsx /uifoo fell through handled=false; and /data was the worst and unpredictable from /auth — GET /datax answered a 200 SUCCESS envelope for a fabricated object (the handler reads substring(5) as an OBJECT NAME) and GET /datax/foo THREW 'Record foo not found in x'. All eleven now answer ROUTE_NOT_FOUND; every canonical prefix and sub-path is unchanged. STOP-AND-REPORT CONDITION CHECKED AND NOT TRIGGERED: no in-repo caller concatenates a domain prefix without a separator (every dispatcher.dispatch call site writes the literal prefix plus a '/'-led sub-path), no route-ledger row names the shape, no SDK method addresses one. COMMANDS: `pnpm --filter @objectstack/runtime test` -> 244 files / 3437 tests passed, os-verify-lock VERDICT command-exit 0. `pnpm --filter @objectstack/runtime typecheck` -> VERDICT command-exit 0 (tsc --noEmit + check:test-typecheck OK). `pnpm --filter '@objectstack/runtime^...' build` -> VERDICT command-exit 0. Repo-wide `eslint . --no-inline-config --format json` -> 6358 files, 0 errors, 0 warnings, exit 0 (full scan, so no narrowing is claimed). Derived gate families via scripts/pm/dispatch-gates.mjs --commands: 56 run, 54 green. NOT MEASURED (not red): check:dual-build-cjs-loads and check:type-check-debt both exited 3 printing their own verdict line 'PREREQUISITE NOT MET ... nothing was measured' — they need a full repo `pnpm build` closure, left to CI. ABLATION: both defects reinstated in the COMMITTED tree -> RED, 3 failed (3) test files, 30 failed / 90 passed. Mutation proved on disk before the run by blob hash (registry 130fb34a -> 25fc93ce, dispatcher 4aa3b054 -> 631fd06f) plus grep counts on the exact injected and deleted text; restored under a trap on EXIT INT TERM using `git checkout HEAD -- path` and verified BY STATE, not by exit code — `git diff HEAD` empty and both blobs byte-identical to HEAD. DOCS-DRIFT HAND-READ: provenance caveat is LIVE and not retired — the bot computed on merge commit feaea9374744b with uncommitted changes, and `git diff --stat 7f96e1417e feaea9374744b -- content/docs` is NOT empty (api/data-api.mdx, plugins/anatomy.mdx, references/api/protocol.mdx differ — other people's landed work; my diff touches no docs), so I read the differing data-api.mdx too and it is unaffected. The checkout is shallow, so no ancestry claim is made anywhere in this reading; the comparison is a tree diff between two present objects, which is exact. Blind-spot search over content/ in short wrap-immune tokens, both rules: claim rule (datax, metaxyz, uifoo, aixx, packagesomething, i18nxx, authx) zero hits, `startsWith` hits are all the ObjectQL/formula/RLS operator and never a routing rule; skip-list rule (/authentication) every hit is a docs-site link, `membership` co-occurring with skip/bypass/exempt yields nothing about path prefixes. The one page the input-shaped search surfaced is content/docs/references/api/dispatcher.mdx ('URL prefix matching', naming no symbol in my diff — exactly the blind-spot shape); NOT falsified for two independent reasons: it is marked AUTO-GENERATED — DO NOT EDIT, and its source is packages/spec/src/api/dispatcher.zod.ts, the spec's declarative DispatcherConfig.routes[].prefix, a different schema from DomainHandlerRegistry — and this PR touches no file in packages/spec.",
      "mcp_calls": "0 — REST was probed first and returned 200, so every read and write went through repo-scoped REST or git; no MCP GitHub call was made for reads, the duplicate search, filing, or the PR.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16839: `isAuthGateAllowlisted` exempts by UNANCHORED substring/suffix — measured with controls, an object named `auth` (/data/auth/123) or a record whose id is `health` (/data/x/health) is exempted from the ADR-0069 auth-policy gate, which the dispatcher reaches by passing cleanPath straight in. Class (a); a different predicate shape in a different package, so a card rather than a rider. Duplicate-searched over 452 open AND closed issues with a control word that hit.",
        "noted, not filed: the independently confirmed census undercounts by three — /keys?, /mcp/skill? and /mcp? also carry the implicit 'prefix' default, deliberately, making 20 registered routes rather than 17. Not a defect; it is the reason the repair is not simply 'make everything a segment'. 承接者: this PR, which declares match: 'prefix' on all three.",
        "noted, not filed: dispatcher-plugin.ts's toScopedPath and mountAiRoute use bare startsWith on the API prefix. Same lexical shape, different role — mount-time route-definition splicing over the plugin's own literals, never a request-path claim — so no defect and no card. 承接者: 无.",
        "noted, not filed: editing a PR body makes the platform append another bare attribution block, so PR #16842's body now carries a duplicate one. Cosmetic platform behaviour, not content loss — the body was read back in full after every write and every section survived. I stopped after one corrective edit rather than loop re-pasting footers. 承接者: 无."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions