mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
docs(server): the Nest README is a blueprint again, and guides may only name paths that exist
server/src/nest/README.md drops from 988 lines to under 200: the module shape with weather as the reference, the cross-cutting pieces, the guards, parity and the test layout, without the bridge playbook and the services recipe that contradicted the code. The migration log moves verbatim to server/docs/nest-migration-history.md, marked historical. scripts/ci/doc-paths.mjs resolves every backtick path in the CLAUDE.md files, the root README, the blueprint and the wiki against the tracked files. The paths already missing are listed in a baseline that only shrinks, and the changes job runs it so docs-only changes are checked too. The wiki no longer says the Playwright flows stay out of CI.
This commit is contained in:
@@ -92,6 +92,14 @@ jobs:
|
||||
- name: Test the classifier
|
||||
run: npm run test:ci-scripts
|
||||
|
||||
- name: Guides only name paths that exist
|
||||
# Every backtick path in the CLAUDE.md files, the root README, the Nest
|
||||
# blueprint and the wiki has to resolve to a tracked file; the ones
|
||||
# already missing are listed in scripts/ci/doc-paths-baseline.json,
|
||||
# which only shrinks. Here rather than in a code job, so a docs-only
|
||||
# change is checked too.
|
||||
run: node scripts/ci/doc-paths.mjs
|
||||
|
||||
- name: List the changed files
|
||||
id: list
|
||||
env:
|
||||
|
||||
@@ -77,7 +77,7 @@ Nest owns everything: every domain is a DI module under `server/src/nest/<domain
|
||||
- **`/mcp` bodies are raw by design** — the parser wrappers exempt `/mcp` so the MCP SDK reads the untouched stream; never `@Body()` a `/mcp` route.
|
||||
- **Auth is default-deny**: global guards run as `APP_GUARD`s; public routes opt out with `@Public()`, and a boot-time ratchet (`validate-route-guards.ts`) refuses any public route not on its allow-list.
|
||||
- **Trip-scoped routes** verify trip access (404) and the permission (403) — `@UseGuards(JwtAuthGuard, TripAccessGuard)` + `@RequirePermission('<action>')` per handler — and forward `X-Socket-Id` to the broadcast so the originating client doesn't echo its own change. **Never gate a multipart upload with a guard** (the client sees ECONNRESET instead of 403 — check in the handler).
|
||||
- **DB** (`server/src/db/`): `better-sqlite3`, WAL, FK on. MikroORM's migrations (`db/migrations/`) are the only schema source, **including for tests** — the hand-kept `schema.ts`/`migrations.ts`/`seeds.ts` test builders are gone. Seeding is MikroORM's too (`db/seeders/`), applied at boot from `buildApp()`; the ORM shares the one better-sqlite3 connection (bound in `db/orm-driver.ts`) rather than opening its own. Queries go through repositories (`server/src/db/repositories/`) and `UnitOfWork` (`server/src/nest/database/unit-of-work.ts`) for transactions — there is no `DatabaseService`. `better-sqlite3` itself is ESLint-restricted to `src/db/` plus a short, named allow-list (`server/eslint.config.mjs`), so a domain reaching for the driver directly fails lint. `NODE_ENV=test` gives each vitest worker an isolated `:memory:` DB.
|
||||
- **DB** (`server/src/db/`): `better-sqlite3`, WAL, FK on. MikroORM's migrations (`db/migrations/`) are the only schema source, **including for tests** — the hand-kept `schema.ts`/`migrations.ts`/`seeds.ts` test builders are gone. Seeding is MikroORM's too (`db/seeders/`), applied at boot from `buildApp()`; the ORM shares the one better-sqlite3 connection (bound in `server/src/db/orm-driver.ts`) rather than opening its own. Queries go through repositories (`server/src/db/repositories/`) and `UnitOfWork` (`server/src/nest/database/unit-of-work.ts`) for transactions — there is no `DatabaseService`. `better-sqlite3` itself is ESLint-restricted to `src/db/` plus a short, named allow-list (`server/eslint.config.mjs`), so a domain reaching for the driver directly fails lint. `NODE_ENV=test` gives each vitest worker an isolated `:memory:` DB.
|
||||
- **Realtime**: `/ws` is a Nest gateway in `server/src/nest/realtime/` — inject `RealtimeService`; `server/src/websocket.ts` is a legacy re-export stub.
|
||||
- **MCP**: tools are declared in `<domain>.mcp.ts` files beside each module, on the `server/src/nest-mcp/` decorator layer. A validation or permission change on a REST route must land in the parallel MCP tool in the same change. See `MCP.md`.
|
||||
- **Addons** are admin-toggleable feature modules keyed by `ADDON_IDS` in `server/src/addons.ts`.
|
||||
@@ -90,7 +90,7 @@ Nest owns everything: every domain is a DI module under `server/src/nest/<domain
|
||||
Offline-first, with a layered data flow. A Page never owns state directly:
|
||||
|
||||
- **Page pattern** (enforced by `lint:pages`, spec in `client/src/pages/PATTERN.md`): a `*Page.tsx` is a **wiring container** composing a co-located `use<Page>()` hook; it must not call React state/effect/memo hooks itself.
|
||||
- **Data flow**: `store (Zustand, client/src/store/) → repo (client/src/repo/) → api (client/src/api/) | Dexie (client/src/db/offlineDb.ts)`. Writes go through `sync/mutationQueue.ts`: optimistic Dexie write, then a replay with an `X-Idempotency-Key` header on reconnect. `store/slices/remoteEventHandler.ts` applies incoming WebSocket events. Components never call the API directly.
|
||||
- **Data flow**: `store (Zustand, client/src/store/) → repo (client/src/repo/) → api (client/src/api/) | Dexie (client/src/db/offlineDb.ts)`. Writes go through `client/src/sync/mutationQueue.ts`: optimistic Dexie write, then a replay with an `X-Idempotency-Key` header on reconnect. `client/src/store/slices/remoteEventHandler.ts` applies incoming WebSocket events. Components never call the API directly.
|
||||
- **Styling**: semantic Tailwind tokens (`bg-surface*`, `text-content*`, `border-edge*`, `bg-accent*`) or the underlying `var(--token)` variables — never color literals, arbitrary-value color classes or numeric inline `fontSize`, so user-chosen scheme/transparency/text-size keep working. `theme:lint` flags bypasses; suppress intentional exceptions (map/PDF/brand) with a `theme-lint-disable` line comment.
|
||||
- **Reading direction**: Arabic runs the app right to left, so spacing, insets and alignment that follow the text use the logical forms (`ms-`/`me-`/`ps-`/`pe-`/`start-`/`end-`/`text-start`/`border-s`/`rounded-s`, `marginInlineStart`, `insetInlineEnd`, `textAlign: 'start'`). `lint:rtl` counts the physical ones per file against `client/scripts/rtl-baseline.json`, which may only shrink; geometry that is physical on purpose in new code (maps, measured positions, time axes) carries an `rtl-lint-disable` comment.
|
||||
- **PWA**: `vite-plugin-pwa` + Workbox cache tiles, API and uploads; `prebuild` generates icons.
|
||||
@@ -126,6 +126,7 @@ These principles come out of a full-repo audit and shape all new code:
|
||||
`.github/workflows/test.yml` runs, per package: typecheck (server also `typecheck:tests`), lint (client through `lint:warnings`, which also holds the warning count per rule), `lint:pages`, `lint:rtl`, `lint:size` (client and server), the server's `lint:boundaries`, `lint:tx`, `lint:dialect` and `lint:test-sql`, the client's `lint:format`, `lint:layers`, `lint:offline`, `lint:dup`, `lint:pairs`, `lint:skips` and `lint:i18n-keys`, shared's `contracts:open`, `check:plugin-facts`, `i18n:parity:strict` (with the untranslated-strings ratchet), the S3 contract suite, the Postgres probe (`postgres-probe`: every dialect helper on SQLite and Postgres, and the statements every repository method reaches with sample arguments against a Postgres service; the SQL errors per method and the list of methods it cannot measure are held to a baseline that only shrinks and fails while it is still unmeasured; see `server/CLAUDE.md`), the browser end-to-end flows (Chromium and WebKit), a client build with `check:gl-split`, coverage for all four packages, then a SonarCloud scan. It also builds the Docker image and boots it until `/api/health/ready` answers (`docker-image`), and lints and renders the Helm chart against the Kubernetes schemas and runs `docker compose config` on every compose file (`deploy-manifests`). `lint-prettier.yml` additionally runs `lint` + `format:check` on `shared/`. `plugin-sdk` has no lint script and is outside the eslint gates. **Not in CI**: `theme:lint` and the screenshot and help-media Playwright projects; run them locally. There are no git hooks; nothing runs before a commit except you. Other workflows build Docker images, scan them, and publish `trek-plugin-sdk` on `plugin-sdk-v*` tags.
|
||||
|
||||
- **`ci-ok` is the required check.** The branch ruleset requires `ci-ok` (plus `lint` and `check-target` from their own workflows), and `ci-ok` needs every job above except the scan. It fails if any of them failed or was cancelled and passes if they passed or were skipped. A new gate job counts only once it is listed in `ci-ok`'s `needs:`.
|
||||
- **Guides name paths that exist**: the `changes` job runs `scripts/ci/doc-paths.mjs`, which resolves every backtick path in the CLAUDE.md files, `README.md`, `server/src/nest/README.md` and the wiki against the tracked files; the missing ones are listed in `scripts/ci/doc-paths-baseline.json`, which only shrinks (`--update` after a fix).
|
||||
- **What runs is decided by `changes`, not by `on.paths`**: `scripts/ci/changed-areas.mjs` maps the changed files to the `code`, `image` and `deploy` areas (tests: `npm run test:ci-scripts`). A path filter on the workflow would leave `ci-ok` unreported on a docs-only PR and block it. Add a new top-level file the build reads to the right area there.
|
||||
- **Runners and Node**: jobs run on `ubuntu-24.04`, not `ubuntu-latest`, and every `setup-node` reads `.nvmrc`. Third-party actions (anything outside `actions/*`) are pinned by commit SHA with the tag in a comment; Dependabot keeps both current.
|
||||
- **Security scans are advisory.** `security.yml` runs `npm audit` (what the image installs, plus `plugin-sdk`) on every PR and push into dev and main, and Docker Scout on the built image for main. Neither is required: a newly published advisory would turn every open PR red at once. Fixes are held as floors in the root `package.json` `overrides`; npm ignores an `overrides` block in a workspace `package.json`, so never put one there.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
[
|
||||
"client/CLAUDE.md :: slices/remoteEventHandler.ts",
|
||||
"plugin-sdk/CLAUDE.md :: protocol/output-contract.ts",
|
||||
"plugin-sdk/CLAUDE.md :: scripts/validate-entry.mjs",
|
||||
"server/CLAUDE.md :: db/migrations.ts",
|
||||
"server/CLAUDE.md :: host/plugin-data.service.ts",
|
||||
"server/CLAUDE.md :: host/rpc-host.ts",
|
||||
"server/CLAUDE.md :: pg-probe/arg-samples.ts",
|
||||
"server/CLAUDE.md :: pg-probe/recorder.ts",
|
||||
"server/CLAUDE.md :: protocol/envelope.ts",
|
||||
"server/CLAUDE.md :: protocol/output-contract.ts",
|
||||
"server/CLAUDE.md :: runtime/egress-policy.ts",
|
||||
"server/CLAUDE.md :: runtime/plugin-host-entry.ts",
|
||||
"wiki/Encryption-Key-Rotation.md :: scripts/migrate-encryption.ts",
|
||||
"wiki/Environment-Variables.md :: scripts/migrate-encryption.ts",
|
||||
"wiki/Environment-Variables.md :: templates/configmap.yaml",
|
||||
"wiki/Environment-Variables.md :: templates/secret.yaml",
|
||||
"wiki/Plugin-Development.md :: client/settings.html",
|
||||
"wiki/Plugin-Development.md :: server/index.js",
|
||||
"wiki/Plugin-Permissions.md :: docs/screenshot.png",
|
||||
"wiki/Plugin-Permissions.md :: server/index.js",
|
||||
"wiki/Plugin-Publishing.md :: docs/screenshot.png",
|
||||
"wiki/Plugin-Publishing.md :: schema/example-entry.json",
|
||||
"wiki/Plugin-Publishing.md :: schema/plugin-entry.schema.json",
|
||||
"wiki/Plugin-Publishing.md :: scripts/check-readme.mjs",
|
||||
"wiki/Plugin-Publishing.md :: scripts/validate-entry.mjs",
|
||||
"wiki/Plugin-Publishing.md :: server/index.js",
|
||||
"wiki/Plugins.md :: server/data/plugins",
|
||||
"wiki/Plugins.md :: server/data/travel.db"
|
||||
]
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
// Every repository path a guide names in backticks has to exist.
|
||||
//
|
||||
// The CLAUDE.md files, the root README, the Nest module blueprint and the wiki
|
||||
// are what a contributor (or an agent) reads before touching the code, and a
|
||||
// path in them is an instruction: open this file, copy that module. When the
|
||||
// file has moved or died, the guide sends the reader to nothing, or worse, to
|
||||
// a recipe for a layer that no longer exists. So this reads each guide,
|
||||
// takes every inline code span that looks like a path into the repository and
|
||||
// checks it against the files git tracks.
|
||||
//
|
||||
// A span counts as a repository path when it has no spaces, globs,
|
||||
// placeholders or URL parts, holds a slash, does not start with `/`, `~` or
|
||||
// `@` (a route, a home path, a package name), and either ends in a file
|
||||
// extension or starts with a directory that exists from one of the bases it
|
||||
// is read against. A trailing `:line` or `:line-line` is ignored. The bases
|
||||
// are the repository root, the guide's own directory, its workspace
|
||||
// (`server/` for anything under server/) and that workspace's `src/`, so
|
||||
// `src/nest/README.md` in server/CLAUDE.md and `nest/database/unit-of-work.ts`
|
||||
// both resolve. Fenced code blocks are not read: they hold commands and
|
||||
// examples, not references.
|
||||
//
|
||||
// The paths that were already missing when the check came in are listed in
|
||||
// scripts/ci/doc-paths-baseline.json as "<guide> :: <path>". The list only
|
||||
// shrinks: a missing path outside it fails, and so does a listed one that
|
||||
// resolves now or whose guide is gone, until --update takes it off.
|
||||
//
|
||||
// node scripts/ci/doc-paths.mjs check against the baseline (CI)
|
||||
// node scripts/ci/doc-paths.mjs --update take resolved entries off the list;
|
||||
// it never adds one
|
||||
// node scripts/ci/doc-paths.mjs --list print every missing path
|
||||
//
|
||||
// It needs nothing but Node and git, so it runs in the changes job before
|
||||
// anything is installed.
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, posix } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fileList, lowerList, readBaseline, runCli, writeBaseline } from '../lib/ratchet.mjs';
|
||||
|
||||
export const WORKSPACES = ['client', 'server', 'shared', 'plugin-sdk'];
|
||||
|
||||
/** The guides the check reads, out of the tracked files. */
|
||||
export function guidesOf(tracked) {
|
||||
return [...tracked]
|
||||
.filter(
|
||||
(path) =>
|
||||
path === 'README.md' ||
|
||||
path === 'server/src/nest/README.md' ||
|
||||
posix.basename(path) === 'CLAUDE.md' ||
|
||||
/^wiki\/[^/]+\.md$/.test(path),
|
||||
)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** The inline code spans of a Markdown text, outside fenced code blocks. */
|
||||
export function inlineSpans(markdown) {
|
||||
const spans = [];
|
||||
let fence = null;
|
||||
for (const line of markdown.replace(/\r\n?/g, '\n').split('\n')) {
|
||||
const marker = /^\s*(`{3,}|~{3,})/.exec(line);
|
||||
if (marker) {
|
||||
if (!fence) fence = marker[1][0];
|
||||
else if (marker[1][0] === fence) fence = null;
|
||||
continue;
|
||||
}
|
||||
if (fence) continue;
|
||||
for (const match of line.matchAll(/(?<!`)`([^`\n]+)`(?!`)/g)) spans.push(match[1].trim());
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
const EXTENSION = /\.(?:md|ts|tsx|mts|cts|js|mjs|cjs|jsx|json|ya?ml|sh|css|sql|html|txt|toml|svg|png|webp)$/;
|
||||
|
||||
/** The directories a guide's relative paths are read against. */
|
||||
export function basesOf(guide) {
|
||||
const dir = posix.dirname(guide);
|
||||
const bases = new Set(['', dir === '.' ? '' : dir]);
|
||||
const workspace = guide.split('/')[0];
|
||||
if (WORKSPACES.includes(workspace)) {
|
||||
bases.add(workspace);
|
||||
bases.add(`${workspace}/src`);
|
||||
}
|
||||
return [...bases];
|
||||
}
|
||||
|
||||
const join2 = (base, path) => posix.normalize(base ? `${base}/${path}` : path).replace(/\/$/, '');
|
||||
|
||||
/**
|
||||
* The span as a repository path to check, or null when it is not one. exists
|
||||
* tells whether a path (file or directory, no trailing slash) is tracked.
|
||||
*/
|
||||
export function pathOf(span, bases, exists) {
|
||||
const path = span.replace(/:\d+(?:-\d+)?$/, '').replace(/[.,;]$/, '');
|
||||
if (!path.includes('/') || /[\s*?<>{}[\]$|=()'"\\,]/.test(path)) return null;
|
||||
if (/^[/~@]/.test(path) || path.includes('://') || /^[a-z]+:/i.test(path)) return null;
|
||||
const first = path.replace(/^(?:\.\.?\/)+/, '').split('/')[0];
|
||||
const fromKnownDir = bases.some((base) => exists(join2(base, first)));
|
||||
if (!EXTENSION.test(path) && !fromKnownDir) return null;
|
||||
return path;
|
||||
}
|
||||
|
||||
/** Module specifiers are often written without their extension (`api/client`). */
|
||||
const IMPLIED = ['', '.ts', '.tsx', '.js', '.mjs'];
|
||||
|
||||
/** Whether the path resolves from one of the bases, as written or as a module specifier. */
|
||||
export function resolves(path, bases, exists) {
|
||||
return bases.some((base) => IMPLIED.some((ext) => exists(join2(base, path) + ext)));
|
||||
}
|
||||
|
||||
/** The missing paths of every guide, as "<guide> :: <path>" entries. */
|
||||
export function missingPaths({ tracked, read }) {
|
||||
const files = new Set(tracked);
|
||||
const dirs = new Set();
|
||||
for (const file of files) {
|
||||
for (let at = file.indexOf('/'); at !== -1; at = file.indexOf('/', at + 1)) dirs.add(file.slice(0, at));
|
||||
}
|
||||
const exists = (path) => files.has(path) || dirs.has(path);
|
||||
const missing = new Set();
|
||||
for (const guide of guidesOf(files)) {
|
||||
const bases = basesOf(guide);
|
||||
for (const span of inlineSpans(read(guide))) {
|
||||
const path = pathOf(span, bases, exists);
|
||||
if (path && !resolves(path, bases, exists)) missing.add(`${guide} :: ${path}`);
|
||||
}
|
||||
}
|
||||
return [...missing].sort();
|
||||
}
|
||||
|
||||
/** Runs the check against the list at baselinePath and returns the exit code. */
|
||||
export function check({
|
||||
root,
|
||||
tracked,
|
||||
baselinePath = join(root, 'scripts/ci/doc-paths-baseline.json'),
|
||||
update = false,
|
||||
list = false,
|
||||
log = console.log,
|
||||
error = console.error,
|
||||
}) {
|
||||
const found = missingPaths({ tracked, read: (path) => readFileSync(join(root, path), 'utf8') });
|
||||
let baseline = readBaseline(baselinePath, fileList);
|
||||
if (update) {
|
||||
baseline = lowerList(baseline, found);
|
||||
writeBaseline(baselinePath, baseline);
|
||||
}
|
||||
const allowed = new Set(baseline);
|
||||
const now = new Set(found);
|
||||
const fresh = found.filter((entry) => !allowed.has(entry));
|
||||
const stale = baseline.filter((entry) => !now.has(entry));
|
||||
|
||||
if (list) for (const entry of found) log(`${allowed.has(entry) ? 'listed' : 'new '} ${entry}`);
|
||||
for (const entry of fresh) {
|
||||
const [guide, path] = entry.split(' :: ');
|
||||
error(`FAIL ${guide} names \`${path}\`, which does not exist. Point it at the file that does, or drop it.`);
|
||||
}
|
||||
for (const entry of stale) {
|
||||
error(`FAIL "${entry}" is listed in scripts/ci/doc-paths-baseline.json but resolves now or its guide is gone.`);
|
||||
}
|
||||
if (stale.length) error('Run node scripts/ci/doc-paths.mjs --update to take it off the list.');
|
||||
log(`doc-paths: ${found.length} missing path(s) in the guides, ${baseline.length} listed in the baseline`);
|
||||
return fresh.length || stale.length ? 1 : 0;
|
||||
}
|
||||
|
||||
function trackedFiles(root) {
|
||||
const out = execFileSync('git', ['-c', 'core.quotePath=false', 'ls-files', '-z'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
return out.split('\0').filter(Boolean);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
const root = fileURLToPath(new URL('../..', import.meta.url));
|
||||
await runCli('doc-paths', (args) =>
|
||||
check({ root, tracked: trackedFiles(root), update: args.includes('--update'), list: args.includes('--list') }),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
import { basesOf, check, guidesOf, inlineSpans, missingPaths, pathOf, resolves } from './doc-paths.mjs';
|
||||
|
||||
const TRACKED = [
|
||||
'CLAUDE.md',
|
||||
'README.md',
|
||||
'server/CLAUDE.md',
|
||||
'server/src/nest/README.md',
|
||||
'server/src/nest/weather/weather.service.ts',
|
||||
'server/src/nest/database/unit-of-work.ts',
|
||||
'server/src/config.ts',
|
||||
'server/tests/e2e/harness.ts',
|
||||
'client/src/api/client.ts',
|
||||
'.github/workflows/test.yml',
|
||||
'wiki/Home.md',
|
||||
'wiki/assets/a.png',
|
||||
'docs/other.md',
|
||||
];
|
||||
|
||||
const exists = (path) => {
|
||||
const files = new Set(TRACKED);
|
||||
return files.has(path) || TRACKED.some((file) => file.startsWith(`${path}/`));
|
||||
};
|
||||
|
||||
describe('doc-paths', () => {
|
||||
it('reads the CLAUDE.md files, the root README, the Nest blueprint and the top-level wiki pages', () => {
|
||||
assert.deepEqual(guidesOf([...TRACKED, 'wiki/sub/Deep.md', 'server/README.md']), [
|
||||
'CLAUDE.md',
|
||||
'README.md',
|
||||
'server/CLAUDE.md',
|
||||
'server/src/nest/README.md',
|
||||
'wiki/Home.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('takes inline code spans and skips fenced blocks of either kind', () => {
|
||||
const md = [
|
||||
'Open `server/src/config.ts` and `a b`.',
|
||||
'```bash',
|
||||
'cat `server/nope.ts`',
|
||||
'```',
|
||||
'~~~',
|
||||
'`server/also-nope.ts`',
|
||||
'~~~',
|
||||
'Then ``double`` and `client/src/api/client.ts`.',
|
||||
].join('\r\n');
|
||||
assert.deepEqual(inlineSpans(md), ['server/src/config.ts', 'a b', 'client/src/api/client.ts']);
|
||||
});
|
||||
|
||||
it('reads a relative path against the guide directory, its workspace and the workspace src', () => {
|
||||
assert.deepEqual(basesOf('server/src/nest/README.md'), ['', 'server/src/nest', 'server', 'server/src']);
|
||||
assert.deepEqual(basesOf('CLAUDE.md'), ['']);
|
||||
assert.deepEqual(basesOf('wiki/Home.md'), ['', 'wiki']);
|
||||
});
|
||||
|
||||
it('only treats a span as a path when it looks like one', () => {
|
||||
const bases = basesOf('server/CLAUDE.md');
|
||||
assert.equal(pathOf('src/nest/README.md', bases, exists), 'src/nest/README.md');
|
||||
assert.equal(pathOf('.github/workflows/test.yml:250-251', [''], exists), '.github/workflows/test.yml');
|
||||
assert.equal(pathOf('nest/database/unit-of-work.ts,', bases, exists), 'nest/database/unit-of-work.ts');
|
||||
assert.equal(pathOf('src/gone/', bases, exists), 'src/gone/');
|
||||
for (const span of [
|
||||
'weather.service.ts',
|
||||
'/api/health/ready',
|
||||
'~/.ssh/key',
|
||||
'@trek/shared',
|
||||
'https://example.com/a.md',
|
||||
'text/plain',
|
||||
'src/**/*.ts',
|
||||
'<domain>/<domain>.rpc.ts',
|
||||
'npm run lint',
|
||||
'a/{b,c}.ts',
|
||||
'C:/tmp/x.ts',
|
||||
]) {
|
||||
assert.equal(pathOf(span, bases, exists), null, span);
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves a path as written or as a module specifier without its extension', () => {
|
||||
assert.equal(resolves('src/config', basesOf('server/CLAUDE.md'), exists), true);
|
||||
assert.equal(resolves('api/client', ['', 'client/src'], exists), true);
|
||||
assert.equal(resolves('weather/weather.service.ts', basesOf('server/src/nest/README.md'), exists), true);
|
||||
assert.equal(resolves('src/nope.ts', basesOf('server/CLAUDE.md'), exists), false);
|
||||
});
|
||||
|
||||
it('lists every missing path once per guide', () => {
|
||||
const texts = {
|
||||
'CLAUDE.md': 'See `server/src/config.ts`, `server/src/gone.ts` and again `server/src/gone.ts`.',
|
||||
'README.md': 'Nothing here.',
|
||||
'server/CLAUDE.md': 'Use `nest/database/unit-of-work.ts` and `tests/e2e/harness.ts`, not `db/migrations.ts`.',
|
||||
'server/src/nest/README.md': '`weather/weather.service.ts` and `src/services/`.',
|
||||
'wiki/Home.md': 'Pictures live in `assets/a.png`; plugins ship `server/index.js`.',
|
||||
};
|
||||
assert.deepEqual(missingPaths({ tracked: TRACKED, read: (path) => texts[path] }), [
|
||||
'CLAUDE.md :: server/src/gone.ts',
|
||||
'server/CLAUDE.md :: db/migrations.ts',
|
||||
'server/src/nest/README.md :: src/services/',
|
||||
'wiki/Home.md :: server/index.js',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('check', () => {
|
||||
let root;
|
||||
afterEach(() => root && rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
function tree(files, baseline) {
|
||||
root = mkdtempSync(join(tmpdir(), 'doc-paths-'));
|
||||
for (const [path, text] of Object.entries({ ...files, 'scripts/ci/doc-paths-baseline.json': JSON.stringify(baseline) })) {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true });
|
||||
writeFileSync(join(root, path), text);
|
||||
}
|
||||
const out = { log: [], error: [] };
|
||||
const run = (opts = {}) =>
|
||||
check({
|
||||
root,
|
||||
tracked: Object.keys(files),
|
||||
log: (l) => out.log.push(l),
|
||||
error: (l) => out.error.push(l),
|
||||
...opts,
|
||||
});
|
||||
return { out, run };
|
||||
}
|
||||
|
||||
it('passes when every missing path is listed, and fails a new one', () => {
|
||||
const listed = tree({ 'CLAUDE.md': '`server/a.ts`', 'server/b.ts': '' }, ['CLAUDE.md :: server/a.ts']);
|
||||
assert.equal(listed.run(), 0);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
const fresh = tree({ 'CLAUDE.md': '`server/a.ts` `server/c.ts`', 'server/b.ts': '' }, ['CLAUDE.md :: server/a.ts']);
|
||||
assert.equal(fresh.run(), 1);
|
||||
assert.match(fresh.out.error.join('\n'), /CLAUDE\.md names `server\/c\.ts`, which does not exist/);
|
||||
});
|
||||
|
||||
it('fails a listed entry that resolves now until --update takes it off, and never adds one', () => {
|
||||
const t = tree({ 'CLAUDE.md': '`server/b.ts` `server/new.ts`', 'server/b.ts': '' }, ['CLAUDE.md :: server/b.ts']);
|
||||
assert.equal(t.run(), 1);
|
||||
assert.match(t.out.error.join('\n'), /"CLAUDE\.md :: server\/b\.ts" is listed .* resolves now/);
|
||||
assert.match(t.out.error.join('\n'), /--update/);
|
||||
assert.equal(t.run({ update: true }), 1);
|
||||
assert.deepEqual(JSON.parse(readFileSync(join(root, 'scripts/ci/doc-paths-baseline.json'), 'utf8')), []);
|
||||
});
|
||||
|
||||
it('prints every missing path with --list, and stops on a broken baseline', () => {
|
||||
const t = tree({ 'CLAUDE.md': '`server/a.ts`', 'server/b.ts': '' }, ['CLAUDE.md :: server/a.ts']);
|
||||
assert.equal(t.run({ list: true }), 0);
|
||||
assert.ok(t.out.log.includes('listed CLAUDE.md :: server/a.ts'));
|
||||
writeFileSync(join(root, 'scripts/ci/doc-paths-baseline.json'), '{');
|
||||
assert.throws(() => t.run(), /not valid JSON/);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+169
-957
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ Each addon extends TREK with functionality beyond the core trip-planning feature
|
||||
|
||||
## Addon list
|
||||
|
||||
The following addons are registered in the system (defined in `server/src/db/seeds.ts`; the TypeScript constant `ADDON_IDS` in `server/src/addons.ts` covers all addons except `naver_list_import`):
|
||||
The following addons are registered in the system (defined in `server/src/db/seeders/AddonSeeder.ts`; the TypeScript constant `ADDON_IDS` in `server/src/addons.ts` covers all addons except `naver_list_import`):
|
||||
|
||||
| Addon ID | Type | Description |
|
||||
|---|---|---|
|
||||
|
||||
@@ -212,7 +212,7 @@ The `@trek/shared` package is the single source of truth for code shared between
|
||||
| `npm run theme:lint` | Flag styling that bypasses the appearance tokens (not run in CI) |
|
||||
| `npm run check:gl-split` | Fail when one built chunk carries both map engines (MapLibre and Mapbox); run after a build |
|
||||
| `npm run build:analyze` | Production build with the bundle analyzer |
|
||||
| `npm run e2e` | Playwright end-to-end tests (local only, not in CI) |
|
||||
| `npm run e2e` | Playwright end-to-end tests; CI runs the public and app projects in Chromium and WebKit, the screenshot and help-media projects are local only |
|
||||
| `npm run shots` | Capture the wiki screenshots with Playwright |
|
||||
| `npm run shots:promote` | Downscale the captured screenshots and move them into `wiki/assets/` |
|
||||
| `npm run help:media` | Record the help-center pictures against the real app on its own ports, so `npm run dev` can keep running |
|
||||
|
||||
Reference in New Issue
Block a user