You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Seven docs links in five published READMEs are spelled /content/docs/..., a path that resolves on none of the three surfaces — and no gate reads a published README's links at all #9632
Found while implementing #9589 (restoring one such link in plugin-audit). Reported rather than fixed: the seven links below are in four other packages' published READMEs, outside that card's file surface.
The defect
A README listed in a package's files array with private unset is what the npm package page renders, and what a reader browsing GitHub sees. Seven links across five published packages are spelled as a repo path rooted at /:
[I18n Best Practices Guide](/content/docs/protocol/kernel/i18n-standard.mdx)
All five packages are published (private unset, README.md in files).
This form resolves on none of the three surfaces, which is what separates it from a merely-ugly link:
npm — a root-relative href resolves against npmjs.com, not the docs site.
GitHub — it resolves against github.com, not against the repo path it looks like.
the docs site itself — apps/docs/lib/source.ts mounts loader({ baseUrl: '/docs' }) over content/docs, so the route for that first entry is /docs/automation/flows. /content/docs/... is not a route, and apps/docs/redirects.mjs contains no /content source to rescue it (grep -n "'/content" apps/docs/redirects.mjs → 0 hits).
Every target file exists — this is purely the URL's spelling, not rot. The pages are reachable; the links are not.
The established convention is the absolute form, already used by packages/create-objectstack/README.md:90 and the blank template README (lines 39, 70, 108): https://docs.objectstack.ai/docs/... with the path taken under content/docs. Verified against four sample targets, all of which exist.
A separate, milder form appears in packages/services/service-knowledge/README.md:8 and packages/plugins/knowledge-ragflow/README.md:5 (](../../../content/docs/protocol/knowledge.mdx)). Those relative paths do resolve on GitHub and npm, but they land a reader on raw MDX source rather than the rendered page. Worth converting in the same pass; not the same severity, and listed here so the sweep is complete rather than as part of the claim above.
The durable half: nothing gates this
Measured, not assumed. A published README's outbound links are read by no gate in this repo:
check:published-readme-exports (scripts/check-published-readme-exports.mjs, added by PR ci: a published README's symbol claims are checked against the package's built exports #9546) is the one gate whose population is published READMEs, and it is deliberately not a docs linter. It reads fenced code blocks only (typescript / ts / tsx / diff / untagged) and makes exactly one claim per import line and call site. Its only occurrences of ]( are in its own header prose and self-test fixtures. It has no notion of a link.
The lychee lane (.github/workflows/check-links.yml) runs over exactly four globs: content/**/*.md, content/**/*.mdx, README.md, ARCHITECTURE.md. packages/**/README.md is not among them.
check:doc-anchors takes its link sources from content/** plus EXTRA_SOURCES = ['README.md', 'ARCHITECTURE.md'] — the root README, not package READMEs.
check:adr-links is scoped to docs/adr/; check:docs-redirects to apps/docs/redirects.mjs.
So a published README can link anywhere, in any spelling, and ship to npm green. That is the half worth more than the seven links: an npm tarball outlives any in-repo correction, and the reader who follows the link is the one person who cannot file a bug about it.
Proposed shape, with sizing
A contained check, in the same spirit as check:published-readme-exports — narrow, one claim, no docs-linting ambition:
For every markdown file a package publishes (its files array, CHANGELOG.md excluded — same population the exports gate already computes), read markdown link destinations and assert:
a destination beginning / is rejected outright with the absolute-form remedy in the message. This is the whole defect above, it needs no filesystem lookup, and it cannot false-positive: no root-relative href is correct in a file rendered off-site.
a destination on docs.objectstack.ai resolves to a real page the way Fumadocs routes it — apps/docs/redirects.mjs's resolver already does exactly this walk (x.mdx / x.md / x/index.mdx / x/index.md) and can be reused rather than re-derived.
optionally, that a #fragment on such a URL names a real heading id — scripts/check-doc-anchors.mjs already computes Fumadocs heading ids with github-slugger plus the [#custom-id] suffix, so this is a call into an existing implementation, not a second slugger.
Assertion 1 alone closes the measured defect class. 2 and 3 are the anti-rot half and are the reason to reuse the two existing resolvers rather than write a third.
Sizing: small-to-medium. The population computation and the fenced-block/prose split already exist in check-published-readme-exports.mjs; the two resolvers already exist. The risk is scope creep into general link-checking, which assertion 1's narrowness is meant to prevent. No baseline should be needed if the seven links are converted in the same PR — worth confirming with a run before committing to that.
Found while implementing #9589 (restoring one such link in
plugin-audit). Reported rather than fixed: the seven links below are in four other packages' published READMEs, outside that card's file surface.The defect
A README listed in a package's
filesarray withprivateunset is what the npm package page renders, and what a reader browsing GitHub sees. Seven links across five published packages are spelled as a repo path rooted at/:packages/services/service-automation/README.md[Flows](/content/docs/automation/flows.mdx)packages/services/service-automation/README.md[Flow Builder Guide](/content/docs/automation/)packages/services/service-automation/README.md[Trigger Reference](/content/docs/references/automation/)packages/services/service-job/README.md[Job Scheduling Guide](/content/docs/kernel/runtime-services/queue-service.mdx)packages/services/service-cache/README.md[Caching Best Practices](/content/docs/kernel/contracts/cache-service.mdx)packages/services/service-analytics/README.md[Analytics Guide](/content/docs/data-modeling/analytics.mdx)packages/services/service-i18n/README.md[I18n Best Practices Guide](/content/docs/protocol/kernel/i18n-standard.mdx)All five packages are published (
privateunset,README.mdinfiles).This form resolves on none of the three surfaces, which is what separates it from a merely-ugly link:
npmjs.com, not the docs site.github.com, not against the repo path it looks like.apps/docs/lib/source.tsmountsloader({ baseUrl: '/docs' })overcontent/docs, so the route for that first entry is/docs/automation/flows./content/docs/...is not a route, andapps/docs/redirects.mjscontains no/contentsource to rescue it (grep -n "'/content" apps/docs/redirects.mjs→ 0 hits).Every target file exists — this is purely the URL's spelling, not rot. The pages are reachable; the links are not.
The established convention is the absolute form, already used by
packages/create-objectstack/README.md:90and the blank template README (lines 39, 70, 108):https://docs.objectstack.ai/docs/...with the path taken undercontent/docs. Verified against four sample targets, all of which exist.A separate, milder form appears in
packages/services/service-knowledge/README.md:8andpackages/plugins/knowledge-ragflow/README.md:5(](../../../content/docs/protocol/knowledge.mdx)). Those relative paths do resolve on GitHub and npm, but they land a reader on raw MDX source rather than the rendered page. Worth converting in the same pass; not the same severity, and listed here so the sweep is complete rather than as part of the claim above.The durable half: nothing gates this
Measured, not assumed. A published README's outbound links are read by no gate in this repo:
check:published-readme-exports(scripts/check-published-readme-exports.mjs, added by PR ci: a published README's symbol claims are checked against the package's built exports #9546) is the one gate whose population is published READMEs, and it is deliberately not a docs linter. It reads fenced code blocks only (typescript / ts / tsx / diff / untagged) and makes exactly one claim per import line and call site. Its only occurrences of](are in its own header prose and self-test fixtures. It has no notion of a link..github/workflows/check-links.yml) runs over exactly four globs:content/**/*.md,content/**/*.mdx,README.md,ARCHITECTURE.md.packages/**/README.mdis not among them.check:doc-anchorstakes its link sources fromcontent/**plusEXTRA_SOURCES = ['README.md', 'ARCHITECTURE.md']— the root README, not package READMEs.check:adr-linksis scoped todocs/adr/;check:docs-redirectstoapps/docs/redirects.mjs.So a published README can link anywhere, in any spelling, and ship to npm green. That is the half worth more than the seven links: an npm tarball outlives any in-repo correction, and the reader who follows the link is the one person who cannot file a bug about it.
Proposed shape, with sizing
A contained check, in the same spirit as
check:published-readme-exports— narrow, one claim, no docs-linting ambition:For every markdown file a package publishes (its
filesarray,CHANGELOG.mdexcluded — same population the exports gate already computes), read markdown link destinations and assert:/is rejected outright with the absolute-form remedy in the message. This is the whole defect above, it needs no filesystem lookup, and it cannot false-positive: no root-relative href is correct in a file rendered off-site.docs.objectstack.airesolves to a real page the way Fumadocs routes it —apps/docs/redirects.mjs's resolver already does exactly this walk (x.mdx/x.md/x/index.mdx/x/index.md) and can be reused rather than re-derived.#fragmenton such a URL names a real heading id —scripts/check-doc-anchors.mjsalready computes Fumadocs heading ids withgithub-sluggerplus the[#custom-id]suffix, so this is a call into an existing implementation, not a second slugger.Assertion 1 alone closes the measured defect class. 2 and 3 are the anti-rot half and are the reason to reuse the two existing resolvers rather than write a third.
Sizing: small-to-medium. The population computation and the fenced-block/prose split already exist in
check-published-readme-exports.mjs; the two resolvers already exist. The risk is scope creep into general link-checking, which assertion 1's narrowness is meant to prevent. No baseline should be needed if the seven links are converted in the same PR — worth confirming with a run before committing to that.Refs: #9589 · PR #9546 · PR #9531