src/: Library source (TypeScript). Key areas:core/: primitives and building blocks- guards:
primitive.ts,object.ts - logic:
logic.ts(and,andAll,or,not,guardIn) - equality:
equals.ts(equals,equalsBy,equalsKey) - parsing:
parse.ts(safeParse,safeParseWith) - nullability:
nullish.ts(nullable,nonNull,nullish,optional,required) - helpers:
define.ts,predicate.ts - combinators:
core/combinators/*(arrayOf,tupleOf,oneOf,recordOf,struct,oneOfValues)
- guards:
types/: shared type utilitiesutils/: internal helpers (e.g.,toBooleanPredicates)- Entry:
src/index.tsre-exports public API (core,types,utils).
tests/: Jest runtime tests, mirroringsrc/paths (e.g.,tests/core/combinators/struct.test.ts).tests-d/:tsdtype tests, mirroringsrc/paths (e.g.,tests-d/core/combinators/struct.test-d.ts).dist/: Build output (ESM.mjs, CJS.js, and.d.ts) generated bytsup.- Config:
tsconfig*.json,tests/tsconfig.json,tests-d/tsconfig.json,tsup.config.ts,jest.config.cjs,.oxlintrc.json.
- Install:
pnpm install - Lint:
pnpm lint— Oxlint over the repo. - Build:
pnpm build— bundles viatsuptodist/(ESM + CJS + types). - Test (runtime):
pnpm test— runs Jest specs intests/. - Test (types):
pnpm test:types— runstsdspecs intests-d/.
CI runs lint, build, unit tests, and type tests on PRs against main.
- Language: TypeScript (
strict: true). Target ES2022; outputs ESM/CJS. - Imports: use alias
@/*for sources (e.g.,import { struct } from '@/core/combinators'). - Formatting: Oxfmt; 2-space indent; single quotes; semicolons.
- Lint: Oxlint config in
.oxlintrc.json. Project rules:complexity≤ 7;no-unused-vars/no-undefwarn; ignoresdist/*. - Files: kebab-case for modules (
core/combinators/tuple.ts), named exports; keep public API re-exported fromsrc/index.ts.
- Frameworks: Jest for runtime behavior;
tsdfor type-level guarantees. - Naming: runtime tests
tests/*.test.ts; type teststests-d/*.test-d.ts. - Placement: mirror source path (e.g.,
src/core/combinators/record.ts→tests/record-of.test.ts). - Run locally:
pnpm test && pnpm test:typesbefore PR.
- Prefer semantic, readable grouping using comment-based suites:
- Section header lines to mimic Jest describe/it hierarchy.
- Pattern:
// ============================================= // describe: <topic> // ============================================= // it: <expectation> // ... assertions via expectType<...>(...)
- Keep one topic per section; add
// Note:lines for caveats when helpful.
- Keep imports at top; declare helper variables close to usage to aid narrowing demos.
- If a single file grows large, it’s acceptable to split by topic (e.g.,
logic-and.test-d.ts,logic-or.test-d.ts).
- Use a dedicated
tests-d/tsconfig.jsonto give VSCode a clean project context:module: NodeNext,moduleResolution: NodeNext,target: ES2022,strict: true,noEmit: true.baseUrl: ".."andpaths: { "@/*": ["src/*"] }to resolve source aliases.- Avoid
types: ["node"]unless@types/nodeis installed; not required for current tests.
- Runtime tests use
tests/tsconfig.jsonextending root config and settingtypes: ["jest"].
- Commits: follow Conventional Commits observed in history:
feat:,fix:,refactor:,test:,chore:. Keep scope concise (e.g.,refactor: split combinators). - PRs: use template; include summary, rationale, and linked issues. Add screenshots if behavior changes.
- Quality bar: ensure
pnpm lint,pnpm build,pnpm test, andpnpm test:typespass locally.
- Node: CI uses Node 22.22.0 and
pnpm@11.2.2. - Public API: prefer adding exports in
src/index.ts; keep internal helpers unexported. - New combinator example: add
src/core/combinators/<name>.ts, export it insrc/core/combinators/index.ts, addtests/<name>.test.tsandtests-d/<name>.test-d.ts. structsupports{ exact?: boolean }to disallow extra keys whenexact: true.- Jest config maps
@/*viamoduleNameMapperand looks undertests/.
- Prefer generics and proper refinements over assertion casts in public APIs.
- Example (
and):const and = <A, B extends A>(precondition: Guard<A>, condition: Refine<A, B>) => (x: unknown): x is B => precondition(x) && condition(x);
- Rationale: preserves inference from the second predicate without
as unknown as.
- Example (
- For collections of guards, convert to boolean predicates for iteration (
or, etc.) via internal helpers (e.g.,toBooleanPredicates). - Keep combinators total and side‑effect free; prefer linear scans for small literal sets (
oneOfValues) andSetlookups for larger ones.
- Purpose: Add JSDoc to public APIs and WHY comments to non-obvious implementations. Leave enough intent so future readers don’t have to guess.
- JSDoc: One-line summary +
@param/@returnsby default. Use@exampleonly for broadly useful or commonly misused functions (optional). Do not use@typeParam. - WHY comments: Explain “why this design/implementation” briefly. Capture performance, compatibility, background, or constraints that are not obvious from code. Place directly above the target code.
- Style: Concise, active voice, consistent terminology. Don’t restate information that TypeScript types already make obvious.
Example — logic combinator (public JSDoc):
/**
* Combines a precondition guard with an additional refinement to narrow the type.
* @param precondition Broad guard evaluated first; short-circuits on failure.
* @param condition Refinement evaluated only when `precondition` passes.
* @returns Predicate that narrows the input to a subtype.
* // @example: include only if this utility is widely used or often misused
*/
export function and<A, B extends A>(
precondition: Guard<A>,
condition: Refine<A, B>
): (x: unknown) => x is B {
/* ... */
}Example — WHY comment (performance/semantics):
// WHY: For small literal sets, a linear scan with Object.is preserves exact
// equality semantics (NaN, +0/-0) without Set overhead. For larger sets,
// switch to Set membership for speed.
const ONE_OF_VALUES_LINEAR_SCAN_MAX = 8;Capture the reason this exists. If there’s time-bound context or deprecation intent, say so.
/**
* Get congested time slots for the expo.
* Background: Implemented for the 2025 Osaka Expo; scheduled for removal later.
*/
const getExpoConfusedTime = (targetDate: Date): Promise<Time[]> => {
// ...
return confusedTimeList;
};Document the interface of the function with @param / @returns. Add @example when it materially helps.
/**
* Get congested time slots for the expo.
* Background: Implemented for the 2025 Osaka Expo; scheduled for removal later.
* @param targetDate Target date to retrieve congested time windows for.
* @returns Array of congested time windows.
*/
const getExpoConfusedTime = (targetDate: Date): Promise<Time[]> => {
/* ... */
};- TODO: Work planned for later
- FIXME: Known issue or temporary workaround
- HACK: Necessary but non-ideal code
- NOTE: Design rationale or reference info
/**
* Get congested time slots for the expo.
* Background: Implemented for the 2025 Osaka Expo.
* TODO: Remove after the event ends.
* @param targetDate Target date to retrieve congested time windows for.
* @returns Array of congested time windows.
*/When using oxlint-disable or @ts-ignore, always include a neighboring WHY comment explaining the suppression.
// HACK: Accept any due to unknown legacy type surface; tracked in issue #123.
// oxlint-disable-next-line typescript/no-explicit-any
function handleLegacyData(data: any) {
// @ts-ignore: Third-party typings are outdated; safe in our usage context.
someLegacyLibrary.doSomething(data);
}Link issues/PRs/specs with @see so readers can follow the context.
/**
* Task executed during build.
* Used to address the following issue.
* @see https://github.com/your-org/your-repo/issues/123
*/Scope guidelines:
- Public exports: Always include JSDoc, and add WHY where non-obvious.
- Internal utilities: Add brief JSDoc/WHY when used across modules or when behavior is tricky.
- Avoid trivial inline comments; concentrate where they prevent misunderstandings or regressions.
Note: See .github/prompts/jsdoc.prompt.md for additional tone/phrasing inspiration.
- Tool versions live in
mise.toml(Node 22.22.0 and pnpm 11.2.2). Runmise exec -- pnpm …ormise run <task>so commands use that toolchain. - The docs site is
pnpm --filter ./docs devon port 3000. Openhttp://localhost:3000. Next.js 16 blocks dev-client resources for127.0.0.1, so code tabs and other client UI do not hydrate on that host. - Docs pages import the workspace package from
dist/. After changingsrc/, runmise exec -- pnpm buildbefore checking the site. next devwritesdocs/AGENTS.mdanddocs/CLAUDE.mdwhen they are missing. Those files are regenerated locally and are not part of the library API.