Skip to content

fix(docs): converge the docs host on the canonical origin, gate included - #10659

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-10641-canonical-docs-host
Aug 21, 2026
Merged

fix(docs): converge the docs host on the canonical origin, gate included#10659
os-elon merged 2 commits into
mainfrom
claude/issue-10641-canonical-docs-host

Conversation

@os-elon

@os-elonos-elon commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10641

The repo named three hosts for one docs site. The maintainer ruled one of them
canonical on 2026-08-21, verbatim and untranslated:

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

This converges the tree on it — including the gate that was prescribing one of
the others.

Could the hosts be measured? No — and here is what turned up instead

This environment's egress proxy denies CONNECT to all three hosts (objectstack.ai,
protocol.objectstack.ai, docs.objectstack.ai — 403 at the gateway, for curl and
for the fetch tool alike), so the live resolve-check the card asked for is
outstanding
. DNS resolves for all three, but overlapping edge IPs on a shared
anycast frontend say nothing about what is served, so that is not offered as a
measurement.

What is available is in-repo evidence, and it answers the question the card was
really asking — aliases, or separate deployments? Commit 5329a65450
("docs: point site links at canonical objectstack.ai", 2026-07-17) says in its own
message:

The apex domain is now primary (www + docs 30x to it, path-preserving), so
prominent links stop going through a redirect hop.

So: aliases, not separate deployments — the naming-consistency fix the card
scoped, not the larger finding it told me to stop on. Every link rewritten here was
followable before and is followable after; only the spelling changes. Confirmation
against the live hosts is still worth doing by someone whose network can reach them.

Why this kept coming back

That July commit converged root README.md, docker/README.md and the docs-site
navbar link. Two things it missed, and one that landed later and undid it:

  • it missed content/docs.site.json's meta.url and docker/Dockerfile;
  • on 2026-08-18 scripts/check-published-readme-links.mjs landed, prescribing
    docs.objectstack.ai in its remedy text. Its own census counted one such link
    at that commit. By 2026-08-21 there were twelve — authors did what the gate told
    them. A gate that prescribes is a gate that propagates, which is why the gate, not
    the links, is the substance of this PR.

The load-bearing file, and the decision made in it

check-published-readme-links.mjs encoded the host in three places — classifier,
remedy text, self-test. They move together here, but not symmetrically, and the
asymmetry is the decision the card asked me to state:

The classifier ACCEPTS the aliases. The remedy PRESCRIBES only the canonical origin.

Accepting is not slack — it is the direction that preserves coverage. Removing a
host from DOCS_HOSTS does not reject it; it reclassifies it as external, a bucket
that is out of scope by name, so the page-resolution and anchor assertions stop
reading it entirely. Tightening the classifier would therefore have deleted the
anti-rot checks from exactly the URLs most likely to rot — the ones already shipped
inside npm tarballs, which outlive any in-repo fix. It would also have rested on an
assumption about alias liveness that I could not measure.

Prescribing narrowly is where the strictness goes. A new assertion 2 makes a
non-canonical origin a finding in its own right, carrying the canonical rewrite of
that exact URL as its remedy. It deliberately does notcontinue: a link can be
both off-convention and dead, and the author should be told both in one run.

So an alias URL is accepted for checking and rejected for authoring. Those two
facts do not conflict, which is why assertion 2 is documented as a convention
claim rather than folded in with the three reachability ones. The gate is now four
assertions, still ordered by cost.

The comparison is on URL.origin, not hostname, so http:// on the canonical host is
also a non-canonical origin and the rewrite fixes the scheme in the same move.

The self-test was verified by ablation, not by inspection

Both legs ran from the committed state, with the mutation confirmed on disk by
grepping the anchor text before and after (a zero-hit sed exits 0 and would
otherwise read as a clean ablation), then restored and re-confirmed byte-identical.
No build step is involved — these are .mjs scripts node runs directly, nothing
resolves through a dist/.

AblationPredictedObserved
CANONICAL_DOCS_ORIGIN reverted to the aliasREDRED — 25 failures, including the behavioural ones (A2 FAILS on an alias origin, A2 SILENT on the canonical origin, A2 does not continue, canonical: … both directions), not only the string pins
DOCS_HOSTS narrowed to the canonical host aloneREDRED — 15 failures, including A2 does not consume the link: an alias page still RESOLVES and A4 still reads the anchor when the origin is an alias

The second is the one that matters: it puts the design decision under test, not the
string. A future edit narrowing DOCS_HOSTS would lose coverage silently — the
findings would simply stop appearing — so the self-test asserts the resolution and
fragment counters, which cannot go green by going quiet.

Before the sweep, the new assertion was also observed firing against the real tree:
12 findings, one per link, each naming its own canonical rewrite.

content/docs.site.json — what consumes meta.url

Nothing in this repo does. Searched by filename, by key, and by content-tree read
path: apps/docs reads only content/docs and content/blog, through
source.config.ts. Site metadata (title, description, icons) is hardcoded in
apps/docs/app/layout.tsx; there is no metadataBase, no sitemap route and no
robots route, and app/og/docs does not read this file.

So the honest statement is that this is a declaration, not a live input — page
metadata and OG tags do not currently flow from it. It still mattered: it is the file
a human reads to learn what the canonical URL is, and it was the source of one of the
three answers. Its siblings (meta.favicon, the navbar "Website" link) already used
the apex; meta.url was the straggler, left behind by that July commit. Set to the
ruled string exactly, so a consumer added later reads the right value.

The rest of the sweep

  • docker/Dockerfile — both comment lines, which point self-hosting readers at
    the alias.
  • 12 links across 9 published READMEs — the entire population the gate flagged.
    It found exactly 12 and 12 were fixed; the gate is green with 0 non-canonical origin(s) and all 12 pages still resolving.
  • A changeset — patch, across the nine packages whose published content changed.
    A README ships inside the npm tarball, so without a republish the old host keeps
    facing every reader of those package pages. That is the same reasoning the gate's
    own header gives for existing at all.

What was deliberately left alone

The create-objectstack scaffold templates. I swept them, then backed them out in
a2bf4b8734 — the second commit here is that reversal, left visible rather than
rebased away. src/templates/blank/{README.md,Dockerfile,docker-compose.yml} are the
declared surface of #10321, which was promoted into sweep anchor #10353, a queued card
whose PR contract reads "zero changes outside the six items" with one closing line
per member. Delivering part of a member from a PR that never named it would have left
that anchor's accounting wrong, and blank/README.md is touched by the open PR #10507
as well. Those files are outside this gate's published-doc population, so nothing goes
red by leaving them. #10321's open question — "dev states which and why" — now has an
answer, and it is recorded on the anchor.

Three further groups are filed rather than swept, each needing a different kind of PR:

Non-goals honoured

apps/docs/next.config.mjs is untouched — #10638 owns it. Nothing outside this repo
changed; if the aliases should stop answering, that is a DNS/Vercel action and the
maintainer's call, not a PR.

Gates

Re-derived after the final commit with node scripts/pm/dispatch-gates.mjs (no path
args — it takes the change set from the merge base itself), then run at a2bf4b8734.
The derived set was re-derived again after the scope reversal, and re-run on the new
head. All green, quoting each gate's own verdict line rather than a shell status:

✓ check:published-readme-links — 152 outbound link(s) across 60 published markdown
file(s): 0 root-relative, 0 non-canonical origin(s), 12 docs-site page(s) resolved
(0 via redirect), 1 anchor(s) verified.
✓ check:published-readme-links --self-test — … all four assertions observed both
FAILING and SILENT — including the host split itself: an alias origin is a FINDING,
and is still resolved and still anchor-checked.
✅ check-doc-anchors: 272 internal #fragment link(s) across 408 source file(s) all
resolve to a real heading
✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).
check-nul-bytes: OK (scanned 6212 text file(s) … no raw ASCII control bytes).
check-type-source-resolution OK — 76 packages with a tsconfig.json scanned
check-test-source-alias OK — 72 packages with tests scanned

Plus check:changeset-gate-self-tests, check:cross-package-test-inputs (both
spellings), check:objectui-changeset, check-adr-0087-registration,
check-changeset-no-major, check-empty-changeset and affected-docs — all exit 0.

check:i18n first refused rather than passing: the fresh worktree's CLI was not built,
and it says so — "Nothing was checked … this result says NOTHING about whether the
committed translation bundles are in sync"
— instead of comparing nothing and calling
it green. Built @objectstack/cli and re-ran: in sync.

Exit codes were captured before any pipe throughout, never read from a $? that a
pipeline had already overwritten, so no verdict quoted here is a tail status wearing
a gate's clothes.


Generated by Claude Code

The repo named three hosts for one docs site. `content/docs.site.json`
declared `protocol.objectstack.ai` as the site's own canonical URL,
`docker/Dockerfile` pointed self-hosting readers at `docs.objectstack.ai`,
and `scripts/check-published-readme-links.mjs` — a gate — *prescribed*
`docs.objectstack.ai` to every author who tripped it. Maintainer ruling,
2026-08-21: 「这个仓的文档站规范 URL 是 https://objectstack.ai」.
The gate is the load-bearing one, because it propagates: it landed on
2026-08-18 with one such link in its own census, and by 2026-08-21 authors
following its remedy text had written twelve. Its classifier, its remedy
strings and its self-test all encoded the host, so all three move together.
The two halves are deliberately asymmetric, and the header says why:
* the classifier ACCEPTS the aliases (`docs.`, `protocol.`, `www.`).
Dropping a host from `DOCS_HOSTS` does not reject it — it reclassifies it
as `external`, which is out of scope by name, so the page and anchor
assertions stop reading it. Tightening there would delete coverage from
exactly the URLs most likely to rot.
* the remedy PRESCRIBES only `https://objectstack.ai`, and a new assertion 2
makes an alias origin a finding in its own right. Accepted for checking,
rejected for authoring.
The 12 published-README links, `docker/Dockerfile`'s two comments, the
`create-objectstack` scaffold templates and `meta.url` are all converged.
Fixes#10641
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GawRwpD44VwBDVy3hs77AX
Backs the three `src/templates/blank/` files out of this PR. They are the
declared surface of #10321, which was promoted into sweep anchor #10353 —
a queued card with its own PR contract ("zero changes outside the six
items", one closing line per member). Sweeping them here would have left
that member half-delivered by a PR that never named it, and `blank/README.md`
is also touched by the open PR #10507.
The canonical host is settled now, so #10321's open question ("dev states
which and why") has an answer; that is recorded on the anchor rather than
acted on here. The gate does not read these files — they are outside the
published-doc population — so nothing goes red by leaving them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GawRwpD44VwBDVy3hs77AX
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 9 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/create-objectstack/README.md, packages/plugins/knowledge-ragflow/README.md, packages/plugins/plugin-audit/README.md, …) — pages documenting those are invisible to this run

Coarse fallback — 25 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 9dd192d48b17e1d60abc47f16bd9d3c0212d237bpackageMentionDocs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Aug 21, 2026
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 07:52
@os-elon
os-elon enabled auto-merge August 21, 2026 07:52
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 7bf3fb7Aug 21, 2026
37 checks passed
@os-elon
os-elon deleted the claude/issue-10641-canonical-docs-host branch August 21, 2026 08:11
os-elon pushed a commit that referenced this pull request Aug 21, 2026
…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
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/mtooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The repo names four canonical docs hosts, and one of them lives inside a gate that instructs authors

2 participants

@os-elon@claude