diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 911f8ab..d35e0f9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,9 +8,9 @@ without drifting into “abstraction for abstraction’s sake”. ## About this project -`@okikio/undent` is a single-module Deno library published to JSR and npm. The entire -public API lives in `mod.ts` — there is no separate build step and no other -source files to edit. +`@okikio/undent` is a single-module Deno library published to JSR and npm. The +entire public API lives in `mod.ts` — there is no separate build step and no +other source files to edit. It does one thing: strip source-code indentation from template literals and strings. The exports fall into three groups: @@ -29,9 +29,9 @@ deno doc --lint mod.ts # validate JSDoc on every public export ``` Always run `deno doc --lint mod.ts` after any change to the public API surface -or its documentation. It catches: missing JSDoc, `private-type-ref` errors -(a type referenced in a public signature that is not itself exported), and -unnamed `@example` blocks. +or its documentation. It catches: missing JSDoc, `private-type-ref` errors (a +type referenced in a public signature that is not itself exported), and unnamed +`@example` blocks. ## Default operating mode @@ -98,12 +98,19 @@ When networking/infra is involved: ## Breaking changes -When making a behavioral change, touch all four of these before closing the task: +When making a behavioral change, touch all four of these before closing the +task: -1. **Confirm all behavioral changes with user** — ask for confirmation on the proposed change and its scope before implementing. Be detailed about what will change and how it will affect the users and the project as a whole, including effects on performance, fragility, reliability, maintainability, and flexibility. +1. **Confirm all behavioral changes with user** — ask for confirmation on the + proposed change and its scope before implementing. Be detailed about what + will change and how it will affect the users and the project as a whole, + including effects on performance, fragility, reliability, maintainability, + and flexibility. 2. **Tests** — update or add assertions that reflect the new behavior. -3. **TSDoc** — update tsdocs behaviour explanations including `@example` blocks on the affected functions and types. -4. **README** — update the relevant docs sections including usage sections with matching examples. +3. **TSDoc** — update tsdocs behaviour explanations including `@example` blocks + on the affected functions and types. +4. **README** — update the relevant docs sections including usage sections with + matching examples. 5. **CHANGELOG** — note the change under the correct version heading. ## Safety / Security / Privacy @@ -128,11 +135,11 @@ When acting as an agent on multi-step work: Targeted rules live under `.github/instructions/`: -| File | Applies to | -|------|------------| -| `typescript.instructions.md` | `**/*.ts`, `**/*.tsx` | -| `markdown-writing.instructions.md` | `**/*.md`, `**/*.ts`, `**/*.tsx` | -| `ascii-diagrams.instructions.md` | `**/*.ts`, `**/*.md` | -| `testing.instructions.md` | `**/*_test.ts`, `**/*.test.ts` | -| `benchmarking.instructions.md` | `**/*_bench.ts`, `**/*bench*.ts` | -| `changelog-commits.instructions.md` | `**` (all files) | +| File | Applies to | +| ----------------------------------- | -------------------------------- | +| `typescript.instructions.md` | `**/*.ts`, `**/*.tsx` | +| `markdown-writing.instructions.md` | `**/*.md`, `**/*.ts`, `**/*.tsx` | +| `ascii-diagrams.instructions.md` | `**/*.ts`, `**/*.md` | +| `testing.instructions.md` | `**/*_test.ts`, `**/*.test.ts` | +| `benchmarking.instructions.md` | `**/*_bench.ts`, `**/*bench*.ts` | +| `changelog-commits.instructions.md` | `**` (all files) | diff --git a/.github/instructions/benchmarking.instructions.md b/.github/instructions/benchmarking.instructions.md index 7552651..8525518 100644 --- a/.github/instructions/benchmarking.instructions.md +++ b/.github/instructions/benchmarking.instructions.md @@ -36,13 +36,17 @@ fast benchmark numbers. Treat any benchmark missing it as broken. The JIT can prove that a template string array (TSA) is always the same frozen object and cache the entire result, hoisting it out of the loop (LICM — Loop -Invariant Code Motion). Use mitata's computed parameter syntax to generate -fresh input values outside the measured region: +Invariant Code Motion). Use mitata's computed parameter syntax to generate fresh +input values outside the measured region: ```ts bench("undent: varying interpolation", function* () { // Inputs are computed outside the measured region: - const value = yield { [0]() { return "some runtime value"; } }; + const value = yield { + [0]() { + return "some runtime value"; + }, + }; // The measured region only runs the function under test: bench(value, (v) => { @@ -56,8 +60,8 @@ by the JIT. Interpolation values are the primary candidate. ## Control GC for allocation-heavy benchmarks -String allocation benchmarks produce unpredictable p99 numbers because random -GC pauses inflate outliers. Use `.gc('inner')` to run GC before each iteration: +String allocation benchmarks produce unpredictable p99 numbers because random GC +pauses inflate outliers. Use `.gc('inner')` to run GC before each iteration: ```ts bench("align: 500-line string", () => { @@ -80,8 +84,8 @@ iteration should use `.gc('inner')`. ## Use `.range()` instead of manual `.args()` for scaling tests -`.range('n', min, max)` auto-generates power-of-2 values, which is cleaner -than manually listing `.args([1, 2, 4, 8, 16, ...])`: +`.range('n', min, max)` auto-generates power-of-2 values, which is cleaner than +manually listing `.args([1, 2, 4, 8, 16, ...])`: ```ts bench("undent: N interpolations", function* (state) { @@ -98,23 +102,29 @@ bench("undent: N interpolations", function* (state) { ## Always benchmark against competitor libraries Performance claims are meaningless without comparison. Every operation that -overlaps with `npm:dedent` and `npm:outdent` must have a side-by-side -benchmark: +overlaps with `npm:dedent` and `npm:outdent` must have a side-by-side benchmark: ```ts import dedent from "npm:dedent"; import outdent from "npm:outdent"; // Benchmark setup shared across all three -const template = (tag) => tag` +const template = (tag) => + tag` hello world goodbye `; -bench("undent (ours)", () => { do_not_optimize(template(undent)); }); -bench("dedent (npm)", () => { do_not_optimize(template(dedent)); }); -bench("outdent (npm)", () => { do_not_optimize(template(outdent)); }); +bench("undent (ours)", () => { + do_not_optimize(template(undent)); +}); +bench("dedent (npm)", () => { + do_not_optimize(template(dedent)); +}); +bench("outdent (npm)", () => { + do_not_optimize(template(outdent)); +}); ``` Use identical inputs. Run them in the same benchmark group so mitata's output @@ -130,8 +140,8 @@ least one benchmark for each of these real-world patterns: multi-line embedded values. - **Config file generation** — small template (5-8 lines), 6-10 key-value interpolations. -- **Hot loop** — the same template called 1 000 times in sequence with - different interpolation values. This also exercises the WeakMap cache. +- **Hot loop** — the same template called 1 000 times in sequence with different + interpolation values. This also exercises the WeakMap cache. - **First-call cost** — a single invocation with a freshly created template strings array (cache cold). Compare to warmed invocations. - **Nested `undent` + `align`** — 2-3 levels of nesting, representative of @@ -142,8 +152,7 @@ least one benchmark for each of these real-world patterns: Ad-hoc heap-delta tests that run outside the benchmark loop produce noisy measurements that aren't comparable across runs. Either: -1. Convert them to mitata benchmarks with `.gc('inner')` so GC is controlled, - or +1. Convert them to mitata benchmarks with `.gc('inner')` so GC is controlled, or 2. Move them to a clearly separate test file and treat them as regression assertions (not performance measurements). @@ -152,13 +161,11 @@ Don't mix manual `performance.memory` checks inside mitata benchmark callbacks. ## Anti-patterns - **Discarding return values** — always `do_not_optimize()` the result. -- **Same literal in every iteration** — use computed parameters to prevent - LICM. -- **Benchmarking only the happy path** — include at least one pathological - input (e.g., deeply nested indentation, very long lines) alongside common - inputs. -- **No competitor baseline** — if you can't show undent is faster than dedent - or outdent on a given operation, don't claim it is. +- **Same literal in every iteration** — use computed parameters to prevent LICM. +- **Benchmarking only the happy path** — include at least one pathological input + (e.g., deeply nested indentation, very long lines) alongside common inputs. +- **No competitor baseline** — if you can't show undent is faster than dedent or + outdent on a given operation, don't claim it is. - **Overhead comparison against raw template literals** — the raw template literal is a zero-cost language feature. The meaningful comparison is undent vs. competitor libraries, not undent vs. nothing. diff --git a/.github/instructions/changelog-commits.instructions.md b/.github/instructions/changelog-commits.instructions.md index 8f3f96c..11cf008 100644 --- a/.github/instructions/changelog-commits.instructions.md +++ b/.github/instructions/changelog-commits.instructions.md @@ -34,8 +34,8 @@ Use a `!` after the type/scope to mark breaking changes: "Removes". 5. Separate from the body with a blank line. -**Imperative mood test:** "If applied, this commit will [subject line]." -Both of these must pass that test: +**Imperative mood test:** "If applied, this commit will [subject line]." Both of +these must pass that test: ``` # Good @@ -62,8 +62,8 @@ fix(cache): prevent stale results when the same template is reused ### Body rules - Wrap at 72 characters. -- Explain **why** the change exists, not what the diff contains (the diff - shows what changed). +- Explain **why** the change exists, not what the diff contains (the diff shows + what changed). - Apply the "5 Whys" rule: if the reason is "it was broken", go one level deeper. Why was it broken? What assumption failed? - Include migration guidance when there is a behavior change. @@ -74,9 +74,9 @@ fix(cache): prevent stale results when the same template is reused ### Atomic commits -One logical change per commit. If you are fixing a bug and refactoring -unrelated code, split them. A commit that cannot be summarized in 50 -characters is probably doing too much. +One logical change per commit. If you are fixing a bug and refactoring unrelated +code, split them. A commit that cannot be summarized in 50 characters is +probably doing too much. When contributing a feature via a pull request, prefer squash-merging with a single well-crafted conventional commit message that represents the changelog @@ -137,8 +137,8 @@ symptom and the result of the fix, not the implementation mechanism. - Fix `dedentString` hanging on strings with mixed `\r\n` and `\r` line endings ``` -Connect changes to broader context when useful. When fixing a long-standing -bug, link to the original issue. For new features, link to the documentation. +Connect changes to broader context when useful. When fixing a long-standing bug, +link to the original issue. For new features, link to the documentation. ### Calling out breaking changes @@ -148,15 +148,15 @@ breaks and what the migration path is: ```md ### Changed -- **Breaking:** `align()` no longer trims trailing whitespace from padded - lines. Callers that relied on the implicit trim must call `.trimEnd()` on - the result explicitly. +- **Breaking:** `align()` no longer trims trailing whitespace from padded lines. + Callers that relied on the implicit trim must call `.trimEnd()` on the result + explicitly. ``` ### The deprecation contract -Deprecations should be visible across at least one version before removal. -The changelog must make the path explicit: +Deprecations should be visible across at least one version before removal. The +changelog must make the path explicit: ```md ## [0.9.0] — deprecates X @@ -180,8 +180,8 @@ explicitly in the changelog rather than deleting the entry: ```md ## [0.8.1] — 2025-01-15 [YANKED] -Yanked due to a regression in `dedentString` that corrupted `\r\n` line -endings. Use 0.8.2 instead. +Yanked due to a regression in `dedentString` that corrupted `\r\n` line endings. +Use 0.8.2 instead. ``` ### Pre-release checklist @@ -189,13 +189,13 @@ endings. Use 0.8.2 instead. Before tagging a release: 1. Rename `[Unreleased]` to the new version with today's date. -2. Read every generated entry. Ask: "would a new user of this package - understand what changed and why?" +2. Read every generated entry. Ask: "would a new user of this package understand + what changed and why?" 3. Group related entries and add context where the commit subject alone is insufficient. 4. Diff the full commit log against the generated entries. Check whether any - `chore`, `refactor`, or `perf` commits actually had user-visible effects - that were miscategorized (changed API timing, dropped support for something, + `chore`, `refactor`, or `perf` commits actually had user-visible effects that + were miscategorized (changed API timing, dropped support for something, altered output format). If so, add them manually. 5. Verify breaking changes are prominent and include a migration path. 6. Verify the narrative reads as a coherent story of deliberate work, not a diff --git a/.github/instructions/markdown-writing.instructions.md b/.github/instructions/markdown-writing.instructions.md index a1a28e3..ba97bc8 100644 --- a/.github/instructions/markdown-writing.instructions.md +++ b/.github/instructions/markdown-writing.instructions.md @@ -10,22 +10,28 @@ applyTo: "**/*.md,**/*.ts,**/*.tsx" Lead with user benefit, not internal mechanics: 1. **What is it?** — one plain-English sentence about what the thing is. -2. **What does it do?** — what problem it solves and what happens when you use it. +2. **What does it do?** — what problem it solves and what happens when you use + it. 3. **What do you get?** — the concrete benefit: cleaner output, less code, fewer bugs, etc. -4. **How does it work?** — the high-level approach, key techniques, and broader intent/reasoning behind the system. -5. **Grounding example & details** — metaphors, charts, diagrams, tables, lists, blockquotes, code examples, assumptions, edge cases, limitations, and other important context. +4. **How does it work?** — the high-level approach, key techniques, and broader + intent/reasoning behind the system. +5. **Grounding example & details** — metaphors, charts, diagrams, tables, lists, + blockquotes, code examples, assumptions, edge cases, limitations, and other + important context. -This order applies to READMEs, JSR/GitHub descriptions, TSDoc for exported -APIs, and any prose that introduces a concept. +This order applies to READMEs, JSR/GitHub descriptions, TSDoc for exported APIs, +and any prose that introduces a concept. ## Writing style -- Write with a clear narrative and smooth transitions. Aim to have a smooth flow and steady pace. Section headers interrupt - flow — prefer prose that moves: intent, context, approach, edge cases, - examples, background, reasoning. -- Use plain English. When technical terms are necessary, define them clearly and ground them in context. -- Headers should be descriptive and functional, not just topical. They should guide the reader through the narrative rather than just labeling sections. +- Write with a clear narrative and smooth transitions. Aim to have a smooth flow + and steady pace. Section headers interrupt flow — prefer prose that moves: + intent, context, approach, edge cases, examples, background, reasoning. +- Use plain English. When technical terms are necessary, define them clearly and + ground them in context. +- Headers should be descriptive and functional, not just topical. They should + guide the reader through the narrative rather than just labeling sections. - Use the subject's name naturally rather than passive constructions. - Expand acronyms on first use and define key terms. - Prefer short sections with clear headers over long walls of text. diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 6fd0f99..f9f0602 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -30,8 +30,8 @@ control in property-based tests. ## Core principle: test behavior, not implementation Treat the module as a black box. Call the public API, assert on the output. -Never assert on internal state, private methods, or implementation details. -A refactor that preserves observable behavior must not break any test. +Never assert on internal state, private methods, or implementation details. A +refactor that preserves observable behavior must not break any test. ## Test independence and determinism @@ -141,8 +141,8 @@ expect(results.every((r) => r.endsWith(" suffix"))).toBe(true); ## Oracle / compatibility tests Don't only test against documented behavior samples. Run `npm:dedent` and -`npm:outdent` on the same randomly generated inputs and assert equivalent -output for the common behavioral subset: +`npm:outdent` on the same randomly generated inputs and assert equivalent output +for the common behavioral subset: ```ts import npmDedent from "npm:dedent"; @@ -160,9 +160,9 @@ This catches behavioral regressions that hand-crafted examples miss. ## Boundary value tests -For any feature with an "N lines" threshold, always test N = 0, 1, 2, and 3. -The `trim` option is the prime example — test 0, 1, and 2 blank lines at each -edge to catch off-by-one mutations: +For any feature with an "N lines" threshold, always test N = 0, 1, 2, and 3. The +`trim` option is the prime example — test 0, 1, and 2 blank lines at each edge +to catch off-by-one mutations: ```ts const trimAll = undent.with({ trim: "all" }); @@ -197,8 +197,8 @@ These are often missed but expose real bugs: would be more robust. Prefer `assertStringIncludes`, line-count checks, or prefix/suffix assertions when exact output isn't what matters. - **Mutation-blind assertions**: a test that runs a code path but never checks - the return value provides false safety. Every `act` step must have an - `assert` step that would fail if the output changed. + the return value provides false safety. Every `act` step must have an `assert` + step that would fail if the output changed. - **Over-abstraction in test helpers**: a helper that builds expected values programmatically using the same logic as the implementation is testing nothing. Expected values should be literals written by a human. diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md index ed87a55..bfbfb3e 100644 --- a/.github/instructions/typescript.instructions.md +++ b/.github/instructions/typescript.instructions.md @@ -55,9 +55,12 @@ applyTo: "**/*.ts,**/*.tsx" For every exported function, interface, type alias, and constant: -- Write TSDoc in plain English — explain *why* it exists, not just *what* it is. Ground the reasoning in the problem being solved, the approach taken, and the assumptions/edge cases. -- Every `@example` block must have a descriptive name that clarifies the scenario and behaviour being demonstrated: - ```ts +- Write TSDoc in plain English — explain _why_ it exists, not just _what_ it is. + Ground the reasoning in the problem being solved, the approach taken, and the + assumptions/edge cases. +- Every `@example` block must have a descriptive name that clarifies the + scenario and behaviour being demonstrated: + ````ts // bad — fails deno doc --lint * @example * ```ts @@ -69,7 +72,7 @@ For every exported function, interface, type alias, and constant: * ```ts * align("hello"); * ``` - ``` + ```` - Include at least two examples for non-trivial APIs: - Example A: common path - Example B: edge case or configuration variant @@ -79,9 +82,12 @@ For every exported function, interface, type alias, and constant: For complex logic, include: -- a docstring summarizing intent, problem, reasoning & logic, purpose + assumptions, -- a step-by-step algorithm explanation (with a walkthrough of the example inputs/outputs), -- make clear what abstract technical codes mean, e.g. binary represents character "C" or keycode represents "Enter", etc..., +- a docstring summarizing intent, problem, reasoning & logic, purpose + + assumptions, +- a step-by-step algorithm explanation (with a walkthrough of the example + inputs/outputs), +- make clear what abstract technical codes mean, e.g. binary represents + character "C" or keycode represents "Enter", etc..., - an ASCII diagram if it improves comprehension. ### Validate with deno doc