Skip to content

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

Description

@os-steve

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 /:

FileLineLink
packages/services/service-automation/README.md155[Flows](/content/docs/automation/flows.mdx)
packages/services/service-automation/README.md394[Flow Builder Guide](/content/docs/automation/)
packages/services/service-automation/README.md395[Trigger Reference](/content/docs/references/automation/)
packages/services/service-job/README.md366[Job Scheduling Guide](/content/docs/kernel/runtime-services/queue-service.mdx)
packages/services/service-cache/README.md292[Caching Best Practices](/content/docs/kernel/contracts/cache-service.mdx)
packages/services/service-analytics/README.md395[Analytics Guide](/content/docs/data-modeling/analytics.mdx)
packages/services/service-i18n/README.md375[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 itselfapps/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:

  1. 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.
  2. 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.
  3. 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.

Refs: #9589 · PR #9546 · PR #9531

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions