diff --git a/Makefile b/Makefile index 4169f189e..53c642acd 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ OPENAPI_SPEC ?= spec/openapi.json # Place one consolidated PHONY declaration near the top of the file .PHONY: all clean be fe fe-build openapi gen docker-up docker-down .PHONY: local-k8s-up local-k8s-down local-k8s-status local-k8s-logs -.PHONY: fmt lint test test-rust test-frontend test-workflow-contracts test-scripts typecheck deps lockfile +.PHONY: fmt lint docs-check test test-rust test-frontend test-workflow-contracts test-scripts typecheck deps lockfile .PHONY: lint-specs audit audit-node rust-audit .PHONY: check-fmt markdownlint markdownlint-docs mermaid-lint nixie yamllint .PHONY: spelling spelling-phrase-check spelling-config spelling-config-write spelling-helper-test @@ -75,7 +75,7 @@ OPENAPI_SPEC ?= spec/openapi.json workspace-sync: ./scripts/sync_workspace_members.py -all: check-fmt lint test spelling +all: check-fmt lint docs-check test spelling clean: cargo clean --manifest-path backend/Cargo.toml @@ -216,6 +216,12 @@ PG_EMBED_SETUP_UNPRIV_VERSION ?= 0.5.1 NEXTEST_TEST_THREADS ?= 1 +# Zero-tolerance documentation gate: TypeDoc's notDocumented validation over +# the frontend-pwa, packages/types, and packages/tokens surfaces (their +# typedoc.json files). Emits no documentation artefacts. +docs-check: + pnpm run docs:check + test: test-rust test-frontend test-scripts test-rust: workspace-sync prepare-pg-worker diff --git a/frontend-pwa/src/api/client.ts b/frontend-pwa/src/api/client.ts index ce31002ee..61d73d103 100644 --- a/frontend-pwa/src/api/client.ts +++ b/frontend-pwa/src/api/client.ts @@ -24,7 +24,9 @@ Object.freeze(usersQueryKey); * */ export const usersQueryKeys = { + /** Key for the whole user collection; alias of {@link usersQueryKey}. */ all: usersQueryKey, + /** Key for a single user, scoped under the collection key. */ byId: (id: User['id']): readonly [...typeof usersQueryKey, User['id']] => { const key = [...usersQueryKey, id] as const; diff --git a/frontend-pwa/src/api/fetcher.ts b/frontend-pwa/src/api/fetcher.ts index 7e2ed37cb..84ec4c821 100644 --- a/frontend-pwa/src/api/fetcher.ts +++ b/frontend-pwa/src/api/fetcher.ts @@ -133,6 +133,10 @@ function getContentTypeForBody(body: unknown): string | null { if (isBinary(body)) return null; return 'application/json'; } +/** + * Fetch wrapper used by the generated Orval client: sets a JSON content type + * only for plain-object bodies and rejects on non-2xx responses. + */ export const customFetch = async (input: string, init?: RequestInit): Promise => { const url = new URL(input, apiBase()); @@ -171,6 +175,10 @@ export const customFetchParsed = async ( return schema.parse(data); }; +/** + * Like `customFetchParsed`, but returns Zod's safe-parse result instead of + * throwing, so callers can branch on validation failure. + */ export const customFetchParsedSafe = async ( input: string, schema: Schema, diff --git a/frontend-pwa/src/app/App.tsx b/frontend-pwa/src/app/App.tsx index f0c7f9ca2..622a74713 100644 --- a/frontend-pwa/src/app/App.tsx +++ b/frontend-pwa/src/app/App.tsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { listUsers, usersQueryKey } from '../api/client'; +/** Root application component: fetches the user list and renders the demo shell. */ export function App() { const { data, isLoading, isError } = useQuery({ queryKey: usersQueryKey, diff --git a/frontend-pwa/typedoc.json b/frontend-pwa/typedoc.json new file mode 100644 index 000000000..b9727906b --- /dev/null +++ b/frontend-pwa/typedoc.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "tsconfig": "tsconfig.json", + "entryPoints": ["src"], + "entryPointStrategy": "expand", + "exclude": [ + "**/*.d.ts", + "**/src/api/generated/**", + "**/*.gen.*", + "**/*.generated.*", + "**/__generated__/**", + "**/*.test.*", + "**/tests/**", + "**/fixtures/**" + ], + "emit": "none", + "commentStyle": "jsdoc", + "excludeExternals": true, + "excludeInternal": true, + "excludePrivate": true, + "excludeProtected": true, + "validation": { + "notDocumented": true, + "notExported": false, + "invalidLink": false, + "invalidPath": false, + "rewrittenLink": false, + "unusedMergeModuleWith": false + }, + "requiredToBeDocumented": [ + "Enum", + "EnumMember", + "Variable", + "Function", + "Class", + "Interface", + "Property", + "Method", + "Accessor", + "TypeAlias" + ], + "treatValidationWarningsAsErrors": true +} diff --git a/package.json b/package.json index d0af66757..a5781538b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "fast-check": "^4.8.0", "markdownlint-cli": "^0.48.0", "puppeteer": "^23.11.1", + "typedoc": "^0.28.20", + "typescript": "5.9.2", "validator": "^13.15.23", "vite": "^7.3.5", "vitest": "^4.1.8" @@ -29,7 +31,8 @@ "test": "vitest run --config vitest.config.mjs", "test:workspaces": "node ./scripts/run-workspace-script.mjs test", "audit": "node ./scripts/run-workspace-script.mjs audit", - "audit:validate": "node security/validate-audit.js" + "audit:validate": "node security/validate-audit.js", + "docs:check": "typedoc --options frontend-pwa/typedoc.json && typedoc --options packages/types/typedoc.json && typedoc --options packages/tokens/typedoc.json" }, "engines": { "node": ">=22.18.0" @@ -68,4 +71,4 @@ "uuid": "14.0.0" } } -} +} \ No newline at end of file diff --git a/packages/tokens/build/validate-contrast.js b/packages/tokens/build/validate-contrast.js index b2bd0b385..186ca4479 100644 --- a/packages/tokens/build/validate-contrast.js +++ b/packages/tokens/build/validate-contrast.js @@ -12,7 +12,10 @@ import { resolveToken } from '../src/utils/tokens.js'; // Load package settings for defaults. /** - * @typedef {{name: string, version: string, contrastThreshold?: number}} PackageJson + * @typedef {object} PackageJson Package manifest fields the contrast validator reads. + * @property {string} name Package name, echoed in validation reports. + * @property {string} version Package version, echoed in validation reports. + * @property {number} [contrastThreshold] Optional override for the minimum contrast ratio. */ /** @type {PackageJson} */ const pkgJson = readJson(new URL('../package.json', import.meta.url)); diff --git a/packages/tokens/src/utils/resolve-token.js b/packages/tokens/src/utils/resolve-token.js index 26167305e..43a668877 100644 --- a/packages/tokens/src/utils/resolve-token.js +++ b/packages/tokens/src/utils/resolve-token.js @@ -173,6 +173,16 @@ function isValidTokenValue(value) { return value !== null && value !== undefined && typeof value === 'string'; } +/** + * Resolve a design-token reference like `{color.brand}` against a token tree. + * + * Follows nested references until a literal value is reached, so aliases of + * aliases resolve; a non-reference string is returned unchanged. + * + * @param {string} ref - Token reference or literal string. + * @param {object} tokens - Token tree supplying `{path: {value}}` leaves. + * @returns {string} The resolved literal token value. + */ export function resolveToken(ref, tokens) { if (typeof ref !== 'string') { throw new TypeError('ref must be a string like "{path.to.token}" or a literal string'); diff --git a/packages/tokens/tsconfig.typedoc.json b/packages/tokens/tsconfig.typedoc.json new file mode 100644 index 000000000..5fa2d5988 --- /dev/null +++ b/packages/tokens/tsconfig.typedoc.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowJs": true, + "checkJs": false, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["build/**/*.js", "build-utils/**/*.js", "src/utils/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/tokens/typedoc.json b/packages/tokens/typedoc.json new file mode 100644 index 000000000..947f6fc3c --- /dev/null +++ b/packages/tokens/typedoc.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "tsconfig": "tsconfig.typedoc.json", + "entryPoints": ["build", "build-utils", "src/utils"], + "entryPointStrategy": "expand", + "exclude": ["**/*.d.ts", "**/node_modules/**"], + "emit": "none", + "commentStyle": "jsdoc", + "excludeExternals": true, + "validation": { + "notDocumented": true, + "notExported": false, + "invalidLink": false, + "invalidPath": false, + "rewrittenLink": false, + "unusedMergeModuleWith": false + }, + "requiredToBeDocumented": ["Variable", "Function", "Class", "Property", "Method", "Accessor"], + "treatValidationWarningsAsErrors": true +} diff --git a/packages/types/dist/src/user.d.ts b/packages/types/dist/src/user.d.ts index 0ac54e4a2..b8159b652 100644 --- a/packages/types/dist/src/user.d.ts +++ b/packages/types/dist/src/user.d.ts @@ -11,7 +11,9 @@ export declare const UserIdSchema: z.ZodBranded; export type UserId = z.infer; /** Runtime schema for a user record. */ export declare const UserSchema: z.ZodObject<{ + /** Branded unique identifier for the user. */ id: z.ZodBranded; + /** Human-readable name; trimmed and guaranteed non-empty by the schema. */ displayName: z.ZodString; }, "strict", z.ZodTypeAny, { id: string & z.BRAND<"UserId">; @@ -24,7 +26,9 @@ export declare const UserSchema: z.ZodObject<{ export type User = z.infer; /** Runtime schema for a list of user records. */ export declare const UsersSchema: z.ZodArray; + /** Human-readable name; trimmed and guaranteed non-empty by the schema. */ displayName: z.ZodString; }, "strict", z.ZodTypeAny, { id: string & z.BRAND<"UserId">; diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index ee7dd980f..ba14147ea 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -14,7 +14,9 @@ export type UserId = z.infer; /** Runtime schema for a user record. */ export const UserSchema = z .object({ + /** Branded unique identifier for the user. */ id: UserIdSchema, + /** Human-readable name; trimmed and guaranteed non-empty by the schema. */ displayName: z.string().trim().min(1, 'displayName must not be empty'), }) .strict(); diff --git a/packages/types/typedoc.json b/packages/types/typedoc.json new file mode 100644 index 000000000..d6aec54a7 --- /dev/null +++ b/packages/types/typedoc.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "tsconfig": "tsconfig.json", + "entryPoints": ["src"], + "entryPointStrategy": "expand", + "exclude": ["**/*.d.ts", "**/dist/**"], + "emit": "none", + "commentStyle": "jsdoc", + "excludeExternals": true, + "excludeInternal": true, + "excludePrivate": true, + "excludeProtected": true, + "validation": { + "notDocumented": true, + "notExported": false, + "invalidLink": false, + "invalidPath": false, + "rewrittenLink": false, + "unusedMergeModuleWith": false + }, + "requiredToBeDocumented": [ + "Enum", + "EnumMember", + "Variable", + "Function", + "Class", + "Interface", + "Property", + "Method", + "Accessor", + "TypeAlias" + ], + "treatValidationWarningsAsErrors": true +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15b65d8bf..abc8c6d4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,12 @@ importers: puppeteer: specifier: ^23.11.1 version: 23.11.1(typescript@5.9.2) + typedoc: + specifier: ^0.28.20 + version: 0.28.20(typescript@5.9.2) + typescript: + specifier: 5.9.2 + version: 5.9.2 validator: specifier: 13.15.23 version: 13.15.23 @@ -529,6 +535,9 @@ packages: '@gerrit0/mini-shiki@3.21.0': resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==} + '@gerrit0/mini-shiki@3.23.0': + resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@headlessui/react@2.2.7': resolution: {integrity: sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==} engines: {node: '>=10'} @@ -878,15 +887,27 @@ packages: '@shikijs/engine-oniguruma@3.21.0': resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + '@shikijs/langs@3.21.0': resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + '@shikijs/themes@3.21.0': resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + '@shikijs/types@3.21.0': resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2980,6 +3001,13 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x + typedoc@0.28.20: + resolution: {integrity: sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==} + engines: {node: '>= 18', pnpm: '>= 10'} + hasBin: true + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -3171,6 +3199,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -3528,6 +3561,14 @@ snapshots: '@shikijs/types': 3.21.0 '@shikijs/vscode-textmate': 10.0.2 + '@gerrit0/mini-shiki@3.23.0': + dependencies: + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@headlessui/react@2.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@floating-ui/react': 0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -3951,19 +3992,37 @@ snapshots: '@shikijs/types': 3.21.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@3.21.0': dependencies: '@shikijs/types': 3.21.0 + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/themes@3.21.0': dependencies: '@shikijs/types': 3.21.0 + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/types@3.21.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -6333,6 +6392,15 @@ snapshots: typescript: 5.9.2 yaml: 2.8.3 + typedoc@0.28.20(typescript@5.9.2): + dependencies: + '@gerrit0/mini-shiki': 3.23.0 + lunr: 2.3.9 + markdown-it: 14.2.0 + minimatch: 10.2.3 + typescript: 5.9.2 + yaml: 2.9.0 + typescript@5.9.2: {} uc.micro@2.1.0: {} @@ -6509,6 +6577,8 @@ snapshots: yaml@2.8.3: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: