From 0d8b6190c53f15f0e832d57846b5dcd88480c38b Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:45:36 +0800 Subject: [PATCH 1/2] docs: refresh README, add docs index and link checker, fix stale paths - README: correct the verify:cheap chain, document verify:pr-local, and rewrite the CI paragraph to match the risk-scoped ci.yml jobs (changes/static-pr/pr-required aggregate, gated release-browser-matrix) - Add docs/README.md: categorized index of all docs distinguishing maintained docs from point-in-time records and archive - Rewrite docs/agents-guide.md as a non-duplicating pointer to AGENTS.md, codebase-index, and the new docs index - Add scripts/check-docs-links.mjs + `npm run docs:check-links` (advisory; not wired into verify chains) to catch dead repo-path references - Fix stale references it found: phantom favourites-home-page.tsx in the site-map generator (regenerated docs/site-map.md), ambiguous src/lib/supabase paths in codebase-index, archived phase-0 link in search-rag-master-context Co-Authored-By: Claude Fable 5 --- README.md | 49 ++++++---- docs/README.md | 93 ++++++++++++++++++ docs/agents-guide.md | 69 ++++++-------- docs/codebase-index.md | 31 +++--- docs/search-rag-master-context.md | 2 +- docs/site-map.md | 2 +- package.json | 1 + scripts/check-docs-links.mjs | 151 ++++++++++++++++++++++++++++++ scripts/generate-site-map.ts | 2 +- 9 files changed, 328 insertions(+), 72 deletions(-) create mode 100644 docs/README.md create mode 100644 scripts/check-docs-links.mjs diff --git a/README.md b/README.md index 924157799..3b17a03c8 100644 --- a/README.md +++ b/README.md @@ -162,33 +162,50 @@ hosted Supabase MCP server uses OAuth, not repo secrets. ## Documentation +Full categorized index: `docs/README.md` (maintained docs vs point-in-time +records vs archive). The most load-bearing entries: + +- `docs/codebase-index.md` — architecture and module map (start here) +- `docs/site-map.md` — generated route map (`npm run sitemap:update`) - `docs/process-hardening.md` — verification gates, CI expectations, known limits - `docs/clinical-governance.md` — deployment and source governance checklist -- `docs/reindex-runbook.md` — safe reindex and ingestion recovery -- `docs/retrieval-quality-runbook.md` — RAG/retrieval eval gates -- `docs/codex-prompt-playbook.md` — copy/paste prompts for common repo work +- `docs/deployment-architecture.md` — app/worker/Supabase deployment topology - `docs/supabase-migration-reconciliation.md` — migration drift and repair policy -- `docs/site-map.md` — generated route map (`npm run sitemap:update`) + +Run `npm run docs:check-links` to verify repo paths referenced from the +maintained docs still resolve. ## Commands Verification gates (see `package.json` for the full chain): ```bash -npm run verify:cheap # check:runtime + sitemap:check + lint + typecheck + test -npm run verify:ui # check:runtime + test:e2e:chromium -npm run verify:release # check:runtime + lint + typecheck + test + build + test:e2e - # + check:production-readiness + governance:release - # + eval:quality:release (needs live Supabase + OpenAI keys) +npm run verify:cheap # check:runtime + check:github-actions + sitemap:check + # + brand:check + check:type-scale + check:icon-scale + # + lint + typecheck + test +npm run verify:pr-local # closest local mirror of the PR gate: format + verify:cheap, + # plus conditional build/client-bundle scan and offline RAG + # tests when changed-file scope requires them +npm run verify:ui # check:runtime + test:e2e:chromium +npm run verify:release # check:runtime + lint + typecheck + test + build + test:e2e + # + check:production-readiness + governance:release + # + eval:quality:release (needs live Supabase + OpenAI keys) ``` -CI runs `format:check` in the `verify` job alongside lint, typecheck, -test:coverage, build, dependency audit, production-readiness CI mode, and -edge-function typecheck. PRs also run Chromium `ui-smoke` and the repo-owned -Supabase `db-reset-verify` job in parallel. The external `Supabase Preview` -check, when enabled, is the branch-database migration replay gate. Docker image -builds, full browser matrix, live drift, and live eval canary checks are -path-filtered, scheduled, or manual rather than normal required checks for every +Use `npm run verify:pr-local -- --dry-run --files ` to +inspect which checks a change would trigger without running them. + +CI is risk-scoped (`.github/workflows/ci.yml`): a `changes` job classifies +changed paths, `static-pr` always runs runtime, action-pin, CI-scope, format, +lint, typecheck, and unit checks, and `pr-required` is the single +always-reporting required aggregate (required PR checks are Gitleaks plus that +aggregate). Coverage, build, safety/config checks, Chromium `ui-critical` +smoke, and the repo-owned Supabase `db-reset-verify` migration replay run only +when their file scopes apply; UI PRs also get a non-blocking advisory +`ui-regression` job. The full Playwright browser matrix +(`release-browser-matrix`) runs on `main`, `release/*`, manual dispatch, and a +weekly schedule. Docker image builds, live drift, and live eval canary checks +are path-filtered, scheduled, or manual rather than required checks for every source-only PR. ```bash diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..9db26464c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,93 @@ +# Clinical KB Documentation Index + +Curated map of everything under `docs/`. Categories distinguish **maintained** +documents (keep these current when behavior changes) from **point-in-time +records** (historical; do not update, supersede with a new dated doc instead). + +Check that repo paths referenced from the maintained docs still resolve with: + +```bash +npm run docs:check-links +``` + +## Start here + +| Doc | What it is | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [codebase-index.md](codebase-index.md) | Structured architecture map: layout, module map, Supabase schema, scripts, domain concepts | +| [site-map.md](site-map.md) | **Generated** route map — regenerate with `npm run sitemap:update`, verify with `npm run sitemap:check` | +| [agents-guide.md](agents-guide.md) | Human onboarding pointer; authoritative agent rules live in the root `AGENTS.md` | + +## Architecture + +- [frontend-architecture.md](frontend-architecture.md) — shell, routing, dashboard module structure +- [deployment-architecture.md](deployment-architecture.md) — app/worker/Supabase deployment topology +- [ingestion-state-machine.md](ingestion-state-machine.md) — ingestion job lifecycle and states +- [design-system.md](design-system.md) — tokens, primitives, styling conventions +- [clinical-chat-ui-component-map.md](clinical-chat-ui-component-map.md) — chat UI component inventory +- [clinical-badge-system-guide.md](clinical-badge-system-guide.md) — clinical badge semantics +- [multi-user-auth-setup.md](multi-user-auth-setup.md) — auth, sessions, owner scoping + +## Operations runbooks + +- [launch-operator-runbook.md](launch-operator-runbook.md) — launch/operational duties and SLO probes +- [reindex-runbook.md](reindex-runbook.md) — safe reindex and ingestion recovery +- [retrieval-quality-runbook.md](retrieval-quality-runbook.md) — RAG/retrieval eval gates and tuning +- [worker-deploy-runbook.md](worker-deploy-runbook.md) — worker build contract, run recipe, secrets +- [disaster-recovery-runbook.md](disaster-recovery-runbook.md) — backup/restore and recovery drills +- [auth-connection-cap-runbook.md](auth-connection-cap-runbook.md) — Supabase auth connection cap (operator) +- [staging-setup.md](staging-setup.md) — staging environment bootstrap +- [database-drift-detection.md](database-drift-detection.md) — schema drift detection (`npm run check:drift`) +- [supabase-migration-reconciliation.md](supabase-migration-reconciliation.md) — migration drift and repair policy +- [observability-slos.md](observability-slos.md) — health probes, SLO counters, degraded modes + +## Governance, safety, privacy + +- [clinical-governance.md](clinical-governance.md) — deployment and source governance checklist +- [clinical-hazard-analysis.md](clinical-hazard-analysis.md) — clinical hazard register +- [rag-injection-threat-model.md](rag-injection-threat-model.md) — prompt-injection threat model +- [privacy-impact-assessment.md](privacy-impact-assessment.md) — PIA findings and launch blockers +- [openai-cross-border-basis.md](openai-cross-border-basis.md) — cross-border data-processing basis +- [production-readiness-checklist.md](production-readiness-checklist.md) — release readiness criteria + +## Process and review + +- [process-hardening.md](process-hardening.md) — verification gates, CI expectations, known debts +- [codex-review-protocol.md](codex-review-protocol.md) — shared review protocol for all review skills +- [codex-prompt-playbook.md](codex-prompt-playbook.md) — copy/paste prompts for common repo work +- [branch-cleanup-guide.md](branch-cleanup-guide.md) — branch hygiene workflow +- [branch-review-ledger.md](branch-review-ledger.md) — reviewed branch/SHA ledger (append after reviews) + +## Plans and workstreams (living) + +- [search-rag-master-plan.md](search-rag-master-plan.md) / [search-rag-master-context.md](search-rag-master-context.md) — search/RAG roadmap and shared context +- [rag-hybrid-findings-and-todo.md](rag-hybrid-findings-and-todo.md) — hybrid retrieval findings backlog +- [reindex-shadow-harness-design.md](reindex-shadow-harness-design.md) — designed-only shadow reindex harness (driver not built) +- [forward-codify-retrieval-rpcs-workorder.md](forward-codify-retrieval-rpcs-workorder.md) — retrieval RPC codification workorder +- [ingestion-concurrency-fix-workorder.md](ingestion-concurrency-fix-workorder.md) — ingestion concurrency workorder +- [project-alignment-cleanup.md](project-alignment-cleanup.md) — repo alignment cleanup tracker +- [redesign/](redesign/) — premium redesign plans, decision log, token adoption +- [superpowers/](superpowers/) — agent-authored plans and specs + +## Point-in-time records (historical — do not update) + +Dated status reports, reviews, and operator decisions. They describe the repo +as it was on that date; supersede with a new dated document rather than editing. + +- [audit/](audit/) — repo and UX/accessibility audits +- [capacity-review.md](capacity-review.md), [scale-readiness-review.md](scale-readiness-review.md), [tenancy-defense-in-depth-review.md](tenancy-defense-in-depth-review.md) +- `*-2026-*` findings and status docs, e.g. [chunking-ocr-reindex-lever-finding-2026-07-08.md](chunking-ocr-reindex-lever-finding-2026-07-08.md), [source-governance-status-2026-07-08.md](source-governance-status-2026-07-08.md), [source-governance-priorities-2026-07-02.md](source-governance-priorities-2026-07-02.md), [source-review-priority-2026-07-02.md](source-review-priority-2026-07-02.md), [operator-apply-july8-batch.md](operator-apply-july8-batch.md) + +## Archive + +- [archive/](archive/) — completed phase plans, superseded designs, and old + progress logs kept for provenance. Never treat archive content as current + guidance. + +## Maintenance rules + +- Generated files (`site-map.md`) are updated only via their generator scripts. +- When adding a doc, add it to the matching section here; date the filename if + it is a point-in-time record. +- When a maintained doc is superseded, move it to `archive/` and update inbound + links (`npm run docs:check-links` finds broken ones). diff --git a/docs/agents-guide.md b/docs/agents-guide.md index b7e935d53..a2e2e2271 100644 --- a/docs/agents-guide.md +++ b/docs/agents-guide.md @@ -1,40 +1,33 @@ # Agents Guide -## Purpose - -This repository contains the Clinical KB application. It is a Next.js + Supabase + OpenAI RAG knowledge base for medical guidelines. Agents working with this project should follow these guidelines. - -## Getting Started - -- Use Node 24 and npm 11; the project uses Next.js v16. -- Copy `.env.example` to `.env.local` and populate secrets. **Never commit actual secrets**. -- Use `npm run ensure` to start or verify the local dev server. It selects a stable port automatically. -- Start the ingestion worker in a separate terminal using `npm run worker`. - -## Development Guidelines - -- Avoid changing `.env.example` values other than adding placeholders. Do not commit `.env.local`. -- When adding environment variables, update the schema in `src/lib/env.ts` and document them in `.env.example`. -- Use TypeScript and follow existing code patterns; avoid introducing new dependencies unless necessary. -- Run `npm run lint` and `npm run typecheck` before committing. -- Use `npm run test` and `npm run test:e2e` to ensure critical flows remain stable. -- Keep Supabase service role keys on the server; never expose them to the client. - -## Routing & Architecture - -- The app uses the Next.js App Router under `src/app`. -- API routes live under `src/app/api`. -- Client components are in `src/components`. -- Supabase integration code is in `src/lib/supabase`. - -## Performance & Safety - -- Use `zod` for request and environment validation. -- Enforce permissions server-side; do not rely on client-side checks. -- Respect the existing rate-limiting and source-governance logic. -- When adding new API endpoints, provide appropriate error handling and status codes. - -## Documentation - -- Keep the README up to date if you change setup, scripts, or environment variables. -- Add high-level architectural changes or decisions in `docs/`. +Short onboarding pointer. The authoritative, always-current rules for agents +working in this repository live in the root [`AGENTS.md`](../AGENTS.md) — +verification gates, provider confirmation boundaries, Supabase project safety, +review routing, and workflow shortcuts. This page only orients you; it does not +duplicate those rules, so it cannot drift from them. + +## Read in this order + +1. [`AGENTS.md`](../AGENTS.md) — agent rules, verification gates, shortcuts + (`upload`, `dependency`, `bug-hunter`), and safety boundaries. +2. [`docs/codebase-index.md`](codebase-index.md) — architecture and module map. +3. [`docs/README.md`](README.md) — index of all runbooks, governance docs, and + plans, with maintained vs historical classification. +4. [`docs/site-map.md`](site-map.md) — generated route map. + +## Human quickstart + +- Node 24.x / npm 11.x are hard requirements (`engine-strict`); the app is + Next.js 16 + Supabase + OpenAI. +- Copy `.env.example` to `.env.local` and fill in values (never commit + secrets). Without Supabase/OpenAI values the app runs in demo mode on a + synthetic corpus. +- `npm run ensure` starts or verifies the dev server on a stable + project-specific port (never assume `localhost:3000`). +- `npm run worker` runs the local ingestion worker in a second terminal. +- When adding environment variables, update the schema in `src/lib/env.ts` and + document them in `.env.example`. +- Before handing off changes: `npm run verify:cheap` first, then + `npm run verify:pr-local` when the change is PR-ready (see + [`docs/process-hardening.md`](process-hardening.md) for the full + verification pyramid). diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 849e71ee3..7e154a691 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -113,12 +113,12 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map ### Supabase, auth, env -| Module | Role | -| ------------------------------------------------------------------------------------ | ---------------------------- | -| `supabase/client.tsx`, `server.ts`, `admin.ts`, `auth.ts`, `health.ts`, `project.ts` | Clients and auth | -| `supabase/database.types.ts` | Generated DB types | -| `env.ts` | Zod-validated environment | -| `owner-scope.ts`, `query-privacy.ts`, `privacy.ts`, `audit.ts` | Multi-user scope and privacy | +| Module | Role | +| ------------------------------------------------------------------------------------------------- | ---------------------------- | +| `src/lib/supabase/` — `client.tsx`, `server.ts`, `admin.ts`, `auth.ts`, `health.ts`, `project.ts` | Clients and auth | +| `src/lib/supabase/database.types.ts` | Generated DB types | +| `env.ts` | Zod-validated environment | +| `owner-scope.ts`, `query-privacy.ts`, `privacy.ts`, `audit.ts` | Multi-user scope and privacy | ### Clinical product data @@ -284,15 +284,16 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement: ## Related docs -| Topic | Doc | -| ----------------------- | ---------------------------------------------------------------- | -| Routes and modes | `docs/site-map.md` | -| Search/RAG roadmap | `docs/search-rag-master-plan.md` | -| Reindex operations | `docs/reindex-runbook.md` | -| Production readiness | `docs/production-readiness-checklist.md` | -| Capacity / scale-up | `docs/capacity-review.md`, `docs/auth-connection-cap-runbook.md` | -| Frontend architecture | `docs/frontend-architecture.md` | -| Repo audit (2026-07-01) | `docs/audit/repo-audit-2026-07-01.md` | +| Topic | Doc | +| ------------------------ | ---------------------------------------------------------------- | +| Full documentation index | `docs/README.md` | +| Routes and modes | `docs/site-map.md` | +| Search/RAG roadmap | `docs/search-rag-master-plan.md` | +| Reindex operations | `docs/reindex-runbook.md` | +| Production readiness | `docs/production-readiness-checklist.md` | +| Capacity / scale-up | `docs/capacity-review.md`, `docs/auth-connection-cap-runbook.md` | +| Frontend architecture | `docs/frontend-architecture.md` | +| Repo audit (2026-07-01) | `docs/audit/repo-audit-2026-07-01.md` | --- diff --git a/docs/search-rag-master-context.md b/docs/search-rag-master-context.md index 5eaa051a6..64f9492a6 100644 --- a/docs/search-rag-master-context.md +++ b/docs/search-rag-master-context.md @@ -35,7 +35,7 @@ Deployment/config note: - `.env.example` documents `OPENAI_ANSWER_TIMEOUT_MS=30000`, matching the server default in `src/lib/env.ts`. - Local `.env.local` may set it explicitly for parity; unset environments rely on the 30000ms server default. -- The historical 12000ms value in `docs/search-rag-phase-0-baseline.md` and `docs/search-rag-master-plan.md` records the Phase 7 rollout, not current guidance. +- The historical 12000ms value in `docs/archive/search-rag-phase-0-baseline.md` and `docs/search-rag-master-plan.md` records the Phase 7 rollout, not current guidance. ## Skill Lenses Used diff --git a/docs/site-map.md b/docs/site-map.md index 17c063780..6bf0e68da 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -604,7 +604,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` | Global shell layouts | `src/app/*/layout.tsx, src/components/clinical-dashboard/global-search-shell.tsx` | | Services | `src/app/services, src/lib/services.ts, src/app/api/registry/records` | | Forms | `src/app/forms, src/lib/forms.ts, src/app/api/registry/records` | -| Favourites | `src/app/favourites, src/components/clinical-dashboard/favourites-home-page.tsx` | +| Favourites | `src/app/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx` | | Differentials | `src/app/differentials, src/lib/differentials.ts` | | Medications | `src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` | | Documents | `src/app/documents, src/lib/document-flow-routes.ts` | diff --git a/package.json b/package.json index da84b2f3d..6a67d1f24 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "check:github-actions": "node scripts/check-github-action-pins.mjs", "check:client-bundle-secrets": "node scripts/check-client-bundle-secrets.mjs", "check:ci-scope": "node scripts/ci-change-scope.mjs --self-test", + "docs:check-links": "node scripts/check-docs-links.mjs", "sitemap:update": "node scripts/run-tsx.mjs scripts/generate-site-map.ts", "sitemap:check": "node scripts/run-tsx.mjs scripts/generate-site-map.ts --check", "brand:update": "node scripts/run-tsx.mjs scripts/generate-brand-assets.ts", diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs new file mode 100644 index 000000000..f2025fa69 --- /dev/null +++ b/scripts/check-docs-links.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +/** + * check-docs-links.mjs — verify that repo-relative paths referenced in the + * maintained documentation surface actually exist. + * + * Scanned by default: README.md, AGENTS.md, and top-level docs/*.md that are + * not dated point-in-time reports (docs/README.md classifies those as + * historical records that intentionally reference the repo as it was). + * Pass --all to also scan dated reports, docs/audit/, docs/redesign/, + * docs/superpowers/, and docs/archive/ (informational; still fails on + * missing paths). + * + * Advisory tool: run `npm run docs:check-links` before doc handoffs. It is + * deliberately NOT part of verify:cheap or CI so historical docs cannot + * block unrelated PRs. + */ + +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const scanAll = process.argv.includes("--all"); + +const ROOT_PREFIXES = [ + "docs/", + "src/", + "scripts/", + "supabase/", + "worker/", + "tests/", + "public/", + ".github/", + ".cursor/", +]; + +// Paths that docs intentionally reference although they do not exist: +// designed-but-unbuilt drivers and hypothetical future splits. +const ALLOWLIST = new Set([ + "scripts/reindex-shadow.ts", // designed-only harness driver (docs/reindex-shadow-harness-design.md) + "docs/site-map.generated.md", // hypothetical future split named in docs/process-hardening.md +]); + +const DATED_DOC = /\b20\d{2}-\d{2}(-\d{2})?\b/; + +function defaultTargets() { + const targets = ["README.md", "AGENTS.md"]; + const docsDir = path.join(repoRoot, "docs"); + for (const entry of readdirSync(docsDir, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith(".md")) { + if (!scanAll && DATED_DOC.test(entry.name)) continue; + targets.push(path.posix.join("docs", entry.name)); + } + if (scanAll && entry.isDirectory()) { + const sub = path.join(docsDir, entry.name); + for (const subEntry of readdirSync(sub, { withFileTypes: true })) { + if (subEntry.isFile() && subEntry.name.endsWith(".md")) { + targets.push(path.posix.join("docs", entry.name, subEntry.name)); + } + } + } + } + return targets; +} + +function candidatePathsFrom(markdown) { + const candidates = new Set(); + + // Inline code spans: `docs/foo.md`, `src/a.ts, src/b.ts`, `path/file.ts:12` + for (const match of markdown.matchAll(/`([^`\n]+)`/g)) { + for (const rawPiece of match[1].split(/,\s*/)) { + candidates.add(rawPiece.trim()); + } + } + + // Markdown link targets: [text](docs/foo.md) + for (const match of markdown.matchAll(/\]\(([^)#\s]+)\)/g)) { + candidates.add(match[1].trim()); + } + + return candidates; +} + +function normalize(candidate) { + let value = candidate; + if (value.startsWith("./")) value = value.slice(2); + // Strip :line, :line:col, or :line-line suffixes. + value = value.replace(/:\d+([-:]\d+)?$/, ""); + return value; +} + +function looksLikeRepoPath(value) { + if (!ROOT_PREFIXES.some((prefix) => value.startsWith(prefix))) return false; + if (/[<>{}$\\]/.test(value)) return false; // templates/placeholders + if (value.includes("*")) return false; // globs checked via their base dir below + if (value.includes("...")) return false; // ellipsis placeholders like src/app/api/... + if (/\s/.test(value)) return false; + // Require a file extension or an explicit trailing slash so that + // non-path tokens sharing a prefix (e.g. the `supabase/postgres` Docker + // image) are not misread as repo paths. Extensionless directory mentions + // are simply skipped, never failed. + const lastSegment = value.replace(/\/$/, "").split("/").pop() ?? ""; + if (!value.endsWith("/") && !lastSegment.includes(".")) return false; + return true; +} + +function globBaseDir(value) { + const starIndex = value.indexOf("*"); + if (starIndex === -1) return null; + const base = value.slice(0, starIndex); + const lastSlash = base.lastIndexOf("/"); + return lastSlash === -1 ? null : base.slice(0, lastSlash); +} + +let missing = 0; +let checked = 0; + +for (const target of defaultTargets()) { + const absoluteTarget = path.join(repoRoot, target); + if (!existsSync(absoluteTarget)) continue; + const markdown = readFileSync(absoluteTarget, "utf8"); + const failures = []; + + for (const rawCandidate of candidatePathsFrom(markdown)) { + const value = normalize(rawCandidate); + if (ALLOWLIST.has(value)) continue; + const base = ROOT_PREFIXES.some((prefix) => value.startsWith(prefix)) ? globBaseDir(value) : null; + if (base !== null) { + checked += 1; + if (!existsSync(path.join(repoRoot, base))) failures.push(`${value} (glob base '${base}' missing)`); + continue; + } + if (!looksLikeRepoPath(value)) continue; + checked += 1; + const cleaned = value.replace(/\/$/, ""); + if (!existsSync(path.join(repoRoot, cleaned))) failures.push(value); + } + + if (failures.length > 0) { + missing += failures.length; + console.error(`\n${target}:`); + for (const failure of failures) console.error(` MISSING ${failure}`); + } +} + +if (missing > 0) { + console.error(`\ndocs link check FAILED: ${missing} missing path(s) across ${checked} checked references.`); + process.exit(1); +} + +console.log(`docs link check passed: ${checked} repo path references resolve.`); diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 397ddfb47..f26d779ed 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -88,7 +88,7 @@ const routeOwnershipRows = [ ["Global shell layouts", "src/app/*/layout.tsx, src/components/clinical-dashboard/global-search-shell.tsx"], ["Services", "src/app/services, src/lib/services.ts, src/app/api/registry/records"], ["Forms", "src/app/forms, src/lib/forms.ts, src/app/api/registry/records"], - ["Favourites", "src/app/favourites, src/components/clinical-dashboard/favourites-home-page.tsx"], + ["Favourites", "src/app/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx"], ["Differentials", "src/app/differentials, src/lib/differentials.ts"], ["Medications", "src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"], ["Documents", "src/app/documents, src/lib/document-flow-routes.ts"], From fa75ccbf89413b6250d7f11957ee43d322410b9b Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:02:18 +0800 Subject: [PATCH 2/2] fix: resolve doc links relative to their file; unbreak image-build postinstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check-docs-links: markdown link targets now resolve relative to the containing file (and accept the repo-root-relative convention), are confined to the repository root, and docs/redesign + docs/superpowers are scanned by default while archive/audit/dated records stay behind --all (review findings from Codex and CodeRabbit) - Dockerfile + Dockerfile.worker: copy scripts/install-git-hooks.mjs into the npm ci stages — main's #583 added it as the npm postinstall hook, so every image build failed at npm ci with the module missing Co-Authored-By: Claude Fable 5 --- Dockerfile | 7 +- Dockerfile.worker | 1 + scripts/check-docs-links.mjs | 132 ++++++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 44 deletions(-) diff --git a/Dockerfile b/Dockerfile index 084661ddc..94b6cc548 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,10 +18,12 @@ FROM node:24-bookworm-slim AS deps WORKDIR /app -# check-node-engine.cjs runs as the npm preinstall hook, so it must be in -# place before `npm ci`. +# check-node-engine.cjs runs as the npm preinstall hook and +# install-git-hooks.mjs as the postinstall hook, so both must be in place +# before `npm ci`. COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs +COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs RUN npm ci FROM node:24-bookworm-slim AS build @@ -40,6 +42,7 @@ FROM node:24-bookworm-slim AS prod-deps WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs +COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs RUN npm ci --omit=dev FROM node:24-bookworm-slim AS runner diff --git a/Dockerfile.worker b/Dockerfile.worker index f35253400..c0669e87c 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -18,6 +18,7 @@ FROM node:24-bookworm-slim AS deps WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs +COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs RUN npm ci FROM node:24-bookworm-slim AS runner diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs index f2025fa69..6493a303e 100644 --- a/scripts/check-docs-links.mjs +++ b/scripts/check-docs-links.mjs @@ -1,14 +1,20 @@ #!/usr/bin/env node /** - * check-docs-links.mjs — verify that repo-relative paths referenced in the - * maintained documentation surface actually exist. + * check-docs-links.mjs — verify that repo paths referenced in the maintained + * documentation surface actually exist. * - * Scanned by default: README.md, AGENTS.md, and top-level docs/*.md that are - * not dated point-in-time reports (docs/README.md classifies those as - * historical records that intentionally reference the repo as it was). - * Pass --all to also scan dated reports, docs/audit/, docs/redesign/, - * docs/superpowers/, and docs/archive/ (informational; still fails on - * missing paths). + * Two kinds of references are checked: + * - Inline code spans (`docs/foo.md`, `src/bar.ts:12`) — treated as + * repo-root-relative paths when they start with a known top-level prefix. + * - Markdown link targets ([text](codebase-index.md), [text](../AGENTS.md)) + * — resolved relative to the file containing the link and required to stay + * inside the repository. + * + * Scanned by default: README.md, AGENTS.md, and docs/**\/*.md excluding + * docs/archive/, docs/audit/, and dated point-in-time filenames + * (docs/README.md classifies those as historical records that intentionally + * reference the repo as it was). Pass --all to scan those too + * (informational deeper sweep; still fails on missing paths). * * Advisory tool: run `npm run docs:check-links` before doc handoffs. It is * deliberately NOT part of verify:cheap or CI so historical docs cannot @@ -39,57 +45,66 @@ const ROOT_PREFIXES = [ const ALLOWLIST = new Set([ "scripts/reindex-shadow.ts", // designed-only harness driver (docs/reindex-shadow-harness-design.md) "docs/site-map.generated.md", // hypothetical future split named in docs/process-hardening.md + // Removed after the redesign; referenced historically in docs/redesign/*: + "src/app/tools/page.tsx", + "src/lib/tools.ts", + "src/components/ServiceDetailPage.tsx", ]); const DATED_DOC = /\b20\d{2}-\d{2}(-\d{2})?\b/; +// Historical directories: only scanned with --all. +const HISTORICAL_DIRS = new Set(["archive", "audit"]); + +function collectDocs(dirRelative, targets) { + const absolute = path.join(repoRoot, dirRelative); + for (const entry of readdirSync(absolute, { withFileTypes: true })) { + const entryRelative = path.posix.join(dirRelative, entry.name); + if (entry.isDirectory()) { + const isHistorical = HISTORICAL_DIRS.has(entry.name); + if (isHistorical && !scanAll) continue; + collectDocs(entryRelative, targets); + continue; + } + if (!entry.isFile() || !entry.name.endsWith(".md")) continue; + if (!scanAll && DATED_DOC.test(entry.name)) continue; + targets.push(entryRelative); + } +} function defaultTargets() { const targets = ["README.md", "AGENTS.md"]; - const docsDir = path.join(repoRoot, "docs"); - for (const entry of readdirSync(docsDir, { withFileTypes: true })) { - if (entry.isFile() && entry.name.endsWith(".md")) { - if (!scanAll && DATED_DOC.test(entry.name)) continue; - targets.push(path.posix.join("docs", entry.name)); - } - if (scanAll && entry.isDirectory()) { - const sub = path.join(docsDir, entry.name); - for (const subEntry of readdirSync(sub, { withFileTypes: true })) { - if (subEntry.isFile() && subEntry.name.endsWith(".md")) { - targets.push(path.posix.join("docs", entry.name, subEntry.name)); - } - } - } - } + collectDocs("docs", targets); return targets; } -function candidatePathsFrom(markdown) { +function codeSpanCandidates(markdown) { const candidates = new Set(); - - // Inline code spans: `docs/foo.md`, `src/a.ts, src/b.ts`, `path/file.ts:12` for (const match of markdown.matchAll(/`([^`\n]+)`/g)) { for (const rawPiece of match[1].split(/,\s*/)) { candidates.add(rawPiece.trim()); } } + return candidates; +} - // Markdown link targets: [text](docs/foo.md) - for (const match of markdown.matchAll(/\]\(([^)#\s]+)\)/g)) { +function linkCandidates(markdown) { + const candidates = new Set(); + for (const match of markdown.matchAll(/\]\(([^)\s]+)\)/g)) { candidates.add(match[1].trim()); } - return candidates; } -function normalize(candidate) { - let value = candidate; - if (value.startsWith("./")) value = value.slice(2); - // Strip :line, :line:col, or :line-line suffixes. - value = value.replace(/:\d+([-:]\d+)?$/, ""); - return value; +function stripSuffixes(value) { + let result = value; + if (result.startsWith("./")) result = result.slice(2); + // Drop #anchor fragments and :line / :line-line / :line:col suffixes. + result = result.replace(/#[^#]*$/, ""); + result = result.replace(/:\d+([-:]\d+)?$/, ""); + return result; } -function looksLikeRepoPath(value) { +function looksLikeRootPath(value) { if (!ROOT_PREFIXES.some((prefix) => value.startsWith(prefix))) return false; if (/[<>{}$\\]/.test(value)) return false; // templates/placeholders if (value.includes("*")) return false; // globs checked via their base dir below @@ -112,6 +127,10 @@ function globBaseDir(value) { return lastSlash === -1 ? null : base.slice(0, lastSlash); } +function isExternalLink(value) { + return /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(value) || value.startsWith("#"); +} + let missing = 0; let checked = 0; @@ -119,21 +138,50 @@ for (const target of defaultTargets()) { const absoluteTarget = path.join(repoRoot, target); if (!existsSync(absoluteTarget)) continue; const markdown = readFileSync(absoluteTarget, "utf8"); + const targetDir = path.posix.dirname(target); const failures = []; - for (const rawCandidate of candidatePathsFrom(markdown)) { - const value = normalize(rawCandidate); - if (ALLOWLIST.has(value)) continue; + const check = (repoRelative, label) => { + if (ALLOWLIST.has(repoRelative)) return; + checked += 1; + const cleaned = repoRelative.replace(/\/$/, ""); + if (!existsSync(path.join(repoRoot, cleaned))) failures.push(label); + }; + + // Inline code spans: repo-root-relative repo paths. + for (const rawCandidate of codeSpanCandidates(markdown)) { + const value = stripSuffixes(rawCandidate); const base = ROOT_PREFIXES.some((prefix) => value.startsWith(prefix)) ? globBaseDir(value) : null; if (base !== null) { + if (ALLOWLIST.has(value)) continue; checked += 1; if (!existsSync(path.join(repoRoot, base))) failures.push(`${value} (glob base '${base}' missing)`); continue; } - if (!looksLikeRepoPath(value)) continue; + if (!looksLikeRootPath(value)) continue; + check(value, value); + } + + // Markdown link targets: repo docs use both repo-root-relative targets + // (`src/lib/env.ts`) and file-relative targets (`codebase-index.md`, + // `../AGENTS.md`). Accept whichever resolves, confined to the repository. + for (const rawCandidate of linkCandidates(markdown)) { + if (isExternalLink(rawCandidate)) continue; + const value = stripSuffixes(rawCandidate); + if (value === "" || value.includes("*") || /[<>{}$\\]/.test(value) || /\s/.test(value)) continue; + const relative = path.posix.normalize(path.posix.join(targetDir === "." ? "" : targetDir, value)); + if (relative.startsWith("..")) { + checked += 1; + failures.push(`${rawCandidate} (escapes repository root)`); + continue; + } + const rootStyle = path.posix.normalize(value); + const candidates = rootStyle === relative || rootStyle.startsWith("..") ? [relative] : [rootStyle, relative]; + if (candidates.some((candidate) => ALLOWLIST.has(candidate))) continue; checked += 1; - const cleaned = value.replace(/\/$/, ""); - if (!existsSync(path.join(repoRoot, cleaned))) failures.push(value); + const found = candidates.some((candidate) => existsSync(path.join(repoRoot, candidate.replace(/\/$/, "")))); + if (!found) + failures.push(rawCandidate === relative ? relative : `${rawCandidate} (tried ${candidates.join(", ")})`); } if (failures.length > 0) {