Skip to content

fix(platform,cli,spec): converge runtime docs URLs on the canonical host, and retarget protection docsUrl at a live page - #10935

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10655-canonical-docs-host
Aug 21, 2026
Merged

fix(platform,cli,spec): converge runtime docs URLs on the canonical host, and retarget protection docsUrl at a live page#10935
os-elon merged 1 commit into
mainfrom
claude/issue-10655-canonical-docs-host

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10655

Follows #10641 / #10659, which converged the docs-site half earlier today (the link
gate, content/docs.site.json, docker/Dockerfile, the published READMEs). This is the
half that lives outside that surface: code that emits text to users. Diffed against
current main first — nothing here re-does or contradicts what #10659 landed.

Maintainer ruling, 2026-08-21, verbatim and untranslated:

这个仓的文档站规范 URL 是 https://objectstack.ai

The routing question, answered first

The card's sequencing constraint was to resolve what serves /adr/...before moving
the host, so the sweep would not just relocate a dead link. Egress is blocked here, so
this was answered from the repo, using the repo's own resolvers rather than a
re-derivation:

ProbeVerdict
/adr/0010-metadata-protectiondocsRelative()null — not in the /docs route space; redirect rescue: none
/adr/0010-metadata-protection-modelsame — null, no rescue
/docs/adr/0010-metadata-protectionin-space but NO PAGE (404) — no such file under content/docs
/docs/references/shared/protectionresolves via content/docs/references/shared/protection.mdx
/docs/references/kernel/metadata-protectionresolves

Four independent facts agree that nothing serves /adr/ on any host:

  1. apps/docs/lib/source.ts mounts content/docs with loader({ baseUrl: '/docs' });
    the only other loader is blog at /blog.
  2. apps/docs/app has no /adr route segment — the route dirs are [lang]/docs,
    [lang]/blog, plus llms.txt / llms.mdx / og / api/search.
  3. apps/docs/redirects.mjs carries 0 sources outside the /docs route space
    asserted independently by check:docs-redirects, whose own success line reads
    0 outside the /docs route space.
  4. The string adr does not appear anywhere in apps/docs/.

There was a third defect underneath the other two: the slug was also wrong. The
record is docs/adr/0010-metadata-protection-model.md; every docsUrl said
0010-metadata-protection. That is the same filename drift scripts/check-adr-links.mjs
already records in its header — a link to ./0010-metadata-protection.md, "a file that
has never existed", which sat long enough for a triage comment to inherit the wrong
attribution and repeat it.

The target chosen, and the one rejected

https://objectstack.ai/docs/references/shared/protection.

The card allowed either the published reference page or a GitHub blob URL, asking that
the choice be judged against what the docsUrl consumer surface actually renders for.
It renders for a browser banner shown to tenant operators:

docsUrl: z.string().url().optional().describe('Optional URL the Studio banner links to for more context.',),

and the TSDoc above it: "Optional documentation link rendered next to the reason in the
Studio lock banner. Use it to point operators at the package's protection policy."
The
reader is an operator who just hit a lock in Studio — not a contributor reading the source
tree. So:

  • The reference page wins on audience. It is the published page for the very schema
    that carries this field, it opens with "Package-level metadata protection (ADR-0010
    §3.7)", and it explains the two-layer protection / _lock model — which is exactly
    the "why is this locked" question the banner is answering.
  • It also wins on rot. A /docs/ destination is covered by
    check:published-readme-links assertion 3 and check:docs-redirects. A
    github.com/.../docs/adr/0010-metadata-protection-model.md blob is covered by nothing
    that would notice a rename — and a rename in exactly this filename is the incident
    already on record above.

/docs/references/kernel/metadata-protection also resolves but documents the private
runtime envelope, which is not the operator's question.

What moved

44 string occurrences across 35 source files, plus one regenerated line.

WhereCountChange
packages/platform-objects/src/identity/*.object.ts22docsUrl → host and path
packages/platform-objects/src/apps/{account,setup,studio}.app.ts3docsUrl → host and path
packages/plugins/plugin-security/src/objects/*.object.ts3docsUrl → host and path
packages/spec/src/shared/protection.zod.ts1TSDoc example → host and path
packages/spec/src/shared/protection.test.ts2fixture URL (identity assertion, unchanged behaviour)
packages/cli/src/utils/spec-version.ts1RELEASES_BASE → host only
packages/cli/src/utils/spec-version.test.ts3pins move with it
packages/apps/setup/src/setup-overview.doc.ts4in-app prose, EN + ZH → host only
packages/apps/studio/src/studio-overview.doc.ts4in-app prose, EN + ZH → host only
examples/app-showcase/src/ui/actions/index.ts1demo action target → host only
content/docs/references/shared/protection.mdx1regenerated, not hand-edited

protection.zod.ts is a generated-reference source, so the reference was regenerated
wholesale (gen:schemagen:openapigen:docs; gen:openapi re-run because
gen:schema's cleanup clears its output). The regeneration moved exactly one line, and
packages/spec/authorable-surface.base.json is unchanged — mechanical confirmation
that no acceptance face moved. Clause-② holds: string values and prose only.

Sweep completeness

After the edits, a repo-wide grep -rn 'docs.objectstack.ai' returns 37 hits in 19
files, zero of them in this PR's surface. Every survivor is deliberate and owned:

SurvivorHitsWhy it stays
scripts/check-published-readme-links.mjs17The gate itself. Its DOCS_HOSTS classifier accepts the aliases by design (#10659's header: dropping one reclassifies it as external, which would delete the page/anchor checks from the URLs most likely to rot — links already shipped in npm tarballs). The rest are self-test fixtures.
skills/objectstack-data/SKILL.md3#10656.skills/** is on GOVERNED_SURFACES; PD #14 means a maintainer-landed PR. Folding it in would fork this whole PR onto that path.
docs/adr/0010-...-model.md, docs/adr/0046-...md2#10656, same reason (docs/adr/** is governed).
packages/create-objectstack/src/templates/blank/{README.md,Dockerfile,docker-compose.yml}5#10321.#10659 explicitly deferred the scaffold templates there.
10 × packages/**/CHANGELOG.md10Historical release prose — never rewritten.
.changeset/canonical-docs-host-in-published-links.md1#10659's own changeset, describing what it changed.

Neighbour-word control for the zero-hit claim (same run, same command shape, so the grep
is demonstrably working rather than silently matching nothing): objectstack.ai 283,
https://objectstack.ai 185, protocol.objectstack.ai 7, www.objectstack.ai 2.

Note for whoever lands #10656:docs/adr/0010-metadata-protection-model.md:565 quotes
the same docsUrl: literal this PR changes in source. #10656 flagged that it should move
with the code rather than ahead of it — the code has now moved, so that line's target is
https://objectstack.ai/docs/references/shared/protection.

Verification

Gate union and tests pinned to final HEAD baea11c39c, clean tree (git status --porcelain → 0 files).

Tests — per package, all green:

PackageResult
@objectstack/spec415 files, 11057 passed
@objectstack/cli147 files, 1624 passed
@objectstack/plugin-security69 files, 1348 passed
@objectstack/platform-objects26 files, 426 passed
@objectstack/example-showcase24 files, 364 passed
@objectstack/setup, @objectstack/studiono test script — verified by reading their package.json, not inferred from an empty run

turbo run typecheck over all seven touched packages (setup and studio included, which is
their real readout): 66/66 tasks successful.

Generated-artifact gates: check:generated"All 14 generated artifacts are up to
date"
; check:authorable-surface"1257 default(s) unchanged"; check:docs"228
generated files in sync with packages/spec"
.

Gate set re-derived with node scripts/pm/dispatch-gates.mjs (no path arguments — it
takes its own change set from the merge base), which named 36 paths and 31 families. Run
and green: check:published-readme-links ("173 outbound link(s) across 60 published
markdown file(s): 0 root-relative, 0 non-canonical origin(s), 27 docs-site page(s)
resolved"
— assertion 2 is the canonical-origin one), check:docs-redirects,
check:doc-anchors, check:doc-frontmatter, check:doc-authoring,
check:docs-audit-scope, check-affected-docs, check:nul-bytes ("scanned 6302 text
file(s) … no raw ASCII control bytes"
), check:i18n, check:examples-live-imports,
check:quick-reference-counts, check:role-word, check:spec-parsed-alias,
check:test-source-alias, check:cross-package-test-inputs, check:merge-driver,
check:engine-double-contract, check:where-matcher, check:query-options-erasure,
check:slot-lookup, check:type-source-resolution, check:ci-filter-parity,
check:plugin-teardown-shape, check:dev-prereqs, spec's check:liveness /
check:empty-state / check:variant-docs / check:strictness-ledger, and the five
changeset families.

The ratchet family was run after the final commit: check:type-check-coverage OK
(64/77 packages type-checked) and check:type-check-debt --re-measure OK — "33 ledger
entr(ies) re-measured in 385.9s, 1908 raw tsc error(s) total, none above its recorded
number."
Its first attempt refused outright because I had built with OS_SKIP_DTS=1 and
@objectstack/service-knowledge had no dist/*.d.ts; that refusal means not measured,
so the closure was rebuilt with declarations and the gate re-run rather than waved
through. It reports a pre-existing -12 surplus on @objectstack/plugin-auth, untouched
by this PR and left for its owner.

Changeset: .changeset/canonical-docs-host-in-runtime-strings.md — patch across the six
published packages whose emitted strings changed. @objectstack/example-showcase is
private and takes none.


Generated by Claude Code

…ost, and retarget protection docsUrl at a live page
Two defects sharing one string literal, in the code that emits text to users.
The host half is spelling ratification. `docs.objectstack.ai` redirects to
`https://objectstack.ai` path-preservingly, so nothing here was broken -- it was
the unratified spelling sitting where users copy from. Maintainer ruling,
2026-08-21: 「这个仓的文档站规范 URL 是 https://objectstack.ai」. #10641/#10659
converged the docs-site half (link gate, docs.site.json, Dockerfile, published
READMEs); these are the references outside that surface.
The path half is a real dead link. All 29 `protection.docsUrl` values pointed at
`/adr/0010-metadata-protection`, and `/adr/...` is not a route on any host:
`apps/docs/lib/source.ts` mounts `content/docs` under `/docs`, `docs/adr/` is not
published, `apps/docs/app` has no `/adr` segment, and `check-docs-redirects`
confirms 0 redirect sources outside the `/docs` route space. The slug was wrong
too -- the record is `0010-metadata-protection-model.md`, the same filename drift
`check-adr-links.mjs`'s header already records. Studio renders this URL in the
lock banner, so an operator asking why an item is locked was sent nowhere.
Retargeted to `https://objectstack.ai/docs/references/shared/protection` -- the
published reference for the very schema carrying the field, and a `/docs/` route
that `check:published-readme-links` assertion 3 and `check:docs-redirects` both
keep alive. A github.com blob of the ADR was the alternative and was rejected:
the consumer is a browser banner aimed at tenant operators, not contributors,
and no gate would notice the blob path rotting.
`packages/spec/src/shared/protection.zod.ts` is a generated-reference source, so
`content/docs/references/shared/protection.mdx` moves with it (one line).
`authorable-surface.base.json` is unchanged -- no acceptance face moved.
Deliberate survivors of the repo-wide sweep, each owned elsewhere:
`skills/objectstack-data/SKILL.md` and the two ADRs (#10656, governed surfaces
needing a maintainer-landed PR); the create-objectstack templates (#10321);
`scripts/check-published-readme-links.mjs`, whose classifier accepts the aliases
by design; and 10 CHANGELOG entries plus #10659's changeset, all historical prose.
Fixes#10655
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 6 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/apps/setup/src/setup-overview.doc.ts, packages/apps/studio/src/studio-overview.doc.ts, packages/spec/src/shared/protection.zod.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3d7deb70005ce2297b5b1a311b629106c3c39bb8packageMentionDocs.

Which tree this was computed on

This run read content/docs from 80c0529fb38d63c57e0e3cd8f737423c08de4ea9 — the merge of head baea11c39ca68c6726d1677a20cdac94395dd336 into base 3d7deb70005ce2297b5b1a311b629106c3c39bb8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 80c0529fb38d63c57e0e3cd8f737423c08de4ea9 && git checkout 80c0529fb38d63c57e0e3cd8f737423c08de4ea9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3d7deb70005ce2297b5b1a311b629106c3c39bb8 baea11c39ca68c6726d1677a20cdac94395dd336 && git checkout -B drift-repro 3d7deb70005ce2297b5b1a311b629106c3c39bb8 && git merge --no-ff baea11c39ca68c6726d1677a20cdac94395dd336
node scripts/docs-audit/affected-docs.mjs --json 3d7deb70005ce2297b5b1a311b629106c3c39bb8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 21, 2026
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 23:41
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review: ACCEPT (reviewer of record: PM seat domain:spec, session_01B4h3medzvhB9rpfoja9jcw)

Verified against GitHub, not the report:

  • Form: Fixes #10655 — correct closure; sequencing constraint 1 (resolve the /adr/ routing question before swapping the host) was honoured, and the answer changed the deliverable rather than being ceremonial: nothing serves /adr/ on any host, so the ~29 protection.docsUrl values moved path as well as host, and the slug was independently wrong. Three defects under one string, all named with evidence.
  • Scope check from the changed-file list (not the report): 35 files, all docs-host strings and their pins; no content/docs/releases/ edit; no unrelated files. The one generated file (content/docs/references/shared/protection.mdx) moved exactly one line and was regenerated by the repo's own generator rather than hand-edited.
  • Changeset: present and correctly shaped — patch across the six published packages the strings ship in, with the reasoning (alias vs canonical host; the /adr/ path fix) written for a release-notes reader. This is the check that mattered here: the Studio lock banner is a user-visible surface, so a missing changeset would have shipped the fix into main and never into a release.
  • Target choice reasoned, not guessed: /docs/references/shared/protection over a GitHub blob because the consumer is a tenant operator in a browser, and because that path is kept alive by check:published-readme-links assertion 3 while a blob path is gated by nothing. That is the "does the benefit survive the boundary" test, answered.
  • Test evidence: per-package suites named with counts; setup/studio correctly reported as having no test script (read from package.json) rather than an empty run passed off as green — the zero-match filter trap avoided explicitly. Head pinned to baea11c39c = PR head, clean tree.
  • Honest failure disclosure: check:type-check-debt --re-measure first REFUSED on an OS_SKIP_DTS=1 build and the dev rebuilt with declarations rather than recording the refusal as a pass. That is exactly the discipline the checklist's "NOT MEASURED ≠ not applicable" rule exists for.
  • Sweep survivors: correctly grouped and each attributed to an existing owner (fix(docs): converge the docs host on the canonical origin, gate included #10659's own classifier by design, Governed surfaces still name docs.objectstack.ai — skills/objectstack-data/SKILL.md (3) and two ADRs — and they need a maintainer-landed PR #10656 for the governed surfaces, [finding] One scaffold run writes three different documentation hostnames — objectstack.ai, objectstack.com, docs.objectstack.ai #10321 for the template files, historical CHANGELOG prose never rewritten). Declining the bounded in-place-fix exemption for the governed-surface hits was the right call — folding them in would have forked this PR onto the human-merge path.

Landing: in the merge queue. On merge: drop pm:dispatched from #10655 and verify closed_by_pull_requests.


Generated by Claude Code

Merged via the queue into main with commit 02b3b07Aug 21, 2026
33 checks passed
@os-elon
os-elon deleted the claude/issue-10655-canonical-docs-host branch August 21, 2026 23:57
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32537778840 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 4.64s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 53 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-elon pushed a commit that referenced this pull request Aug 22, 2026
…nonical host
The scaffolded blank template's README.md linked docs.objectstack.ai (an
accepted-but-unratified alias) in three places, while the root README.md
already used the ruled canonical origin, https://objectstack.ai. One
`npm create objectstack@latest` run therefore handed the newcomer two
different documentation hostnames.
Maintainer ruling, 2026-08-21: 「这个仓的文档站规范 URL 是 https://objectstack.ai」.
scripts/check-published-readme-links.mjs's CANONICAL_DOCS_ORIGIN prescribes
the same origin, and #10659/#10935 already converged the docs-site build
config and the runtime-emitted docsUrl strings onto it — 02b3b07 explicitly
deferred the create-objectstack templates to this issue (#10321).
Fixes#10321
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-elon@claude