untodo 0.0.1-alpha.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +226 -15
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # untodo
2
2
 
3
+ Type-safe TODO for humans and AI — trackable, structured, and lint-enforceable.
4
+
3
5
  Type-safe replacement for `// TODO:` comments.
4
6
  Trackable in your IDE, enforceable by lint, surfaced by the type system.
5
7
 
@@ -16,46 +18,178 @@ Trackable in your IDE, enforceable by lint, surfaced by the type system.
16
18
 
17
19
  Inspired by [Kotlin's `TODO()`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-t-o-d-o.html).
18
20
 
21
+ ## Affinity with AI coding assistants
22
+
23
+ Now that AI-assisted coding is the norm, `// TODO:` comments have a new set of problems. **AI reads comments as plain text**, so it tends to miss the fact that a TODO exists, and the way TODOs are written drifts from author to author.
24
+
25
+ untodo addresses both issues.
26
+
27
+ ### AI recognizes TODOs more reliably
28
+
29
+ A `TODO` in untodo is a function call backed by a type definition. AI coding assistants (Claude, GitHub Copilot, etc.) take type information and function signatures into account as context, so they pick up the existence and intent of a TODO far more reliably than from a `// TODO:` comment.
30
+
31
+ ```typescript
32
+ // AI sees this only as "text"
33
+ // TODO: implement later (where? who? why?)
34
+
35
+ // AI sees this as "a call to an unimplemented function"
36
+ return TODO({ reason: "API not implemented yet", issue: 123, assignee: "alice" })
37
+ ```
38
+
39
+ Structured metadata like `reason`, `issue`, and `assignee` lets AI accurately read off who left the TODO, why, and which issue it is tied to.
40
+
41
+ ### TODOs become consistent in style
42
+
43
+ `// TODO:` comments tend to be written differently by every author.
44
+
45
+ ```typescript
46
+ // TODO: fix this
47
+ // TODO(alice): fix later
48
+ // TODO #123
49
+ // FIXME: has a bug
50
+ ```
51
+
52
+ With untodo, ESLint / oxlint rules can ban comment-style TODOs and enforce the function-call form. Consistent shape also improves AI completion and suggestion quality when it reads the surrounding code.
53
+
19
54
  ## Install
20
55
 
21
56
  ```bash
22
57
  npm install untodo
23
58
  ```
24
59
 
25
- ## Usage
60
+ ## Quick start
61
+
62
+ Run the bundled initializer once at the project root:
63
+
64
+ ```bash
65
+ npx untodo init
66
+ ```
67
+
68
+ This writes two files (and prints an ESLint snippet you can paste into your existing config). Existing files are skipped unless you pass `--force`.
69
+
70
+ ```
71
+ wrote: ./untodo.config.ts
72
+ wrote: ./global.d.ts
73
+ ```
74
+
75
+ ### 1. `untodo.config.ts` — runtime configuration
76
+
77
+ Registers project-wide defaults via `defineConfig`. Per-call callbacks passed to `TODO()` / `FIXME()` / `HACK()` always win over what's set here, so this file is the right place to put fallback behavior.
78
+
79
+ The generated template ships with everything commented out:
26
80
 
27
81
  ```ts
28
- import { TODO, FIXME, HACK, defineConfig } from "untodo";
82
+ import { defineConfig } from 'untodo';
29
83
 
30
- function fetchUser(): User {
31
- return TODO({ reason: "未実装" });
32
- }
84
+ export default defineConfig({
85
+ // repo: 'org/repo',
86
+ // onTodo: (meta) => console.warn(`TODO: ${meta.reason}`),
87
+ // onFixme: (meta) => console.error(`FIXME: ${meta.reason}`),
88
+ // onHack: (meta) => console.warn(`HACK: ${meta.reason}`),
89
+ });
33
90
  ```
34
91
 
35
- `untodo.config.ts`:
92
+ | Field | Type | Purpose |
93
+ |---|---|---|
94
+ | `repo` | `string` | Repository in `org/repo` form. Tooling uses it to expand the `issue` meta field into a full URL (e.g. `https://github.com/org/repo/issues/123`). |
95
+ | `onTodo` | `(meta: TodoMeta) => void` | Default handler for every `TODO()` call that doesn't pass its own callback. |
96
+ | `onFixme` | `(meta: FixmeMeta) => void` | Default handler for every `FIXME()` call that doesn't pass its own callback. |
97
+ | `onHack` | `(meta: HackMeta) => void` | Default handler for every `HACK()` call that doesn't pass its own callback. |
98
+
99
+ A typical setup that warns in development and integrates with your logger:
36
100
 
37
101
  ```ts
38
- import { defineConfig } from "untodo";
102
+ import { defineConfig } from 'untodo';
39
103
 
40
104
  export default defineConfig({
41
- repo: "org/repo",
42
- onTodo: (meta) => console.warn(`TODO: ${meta.reason}`),
105
+ repo: 'ysknsid25/untodo',
106
+ onTodo: (meta) => {
107
+ const issue = meta.issue ? ` (#${meta.issue})` : '';
108
+ console.warn(`TODO${issue}: ${meta.reason}`);
109
+ },
110
+ onFixme: (meta) => console.error(`FIXME: ${meta.reason}`),
111
+ onHack: (meta) => console.warn(`HACK: ${meta.reason}`),
43
112
  });
44
113
  ```
45
114
 
46
- ## ESLint plugin
115
+ Import this file once from your application entry point (or test setup) so `defineConfig` runs before any `TODO()` is evaluated:
116
+
117
+ ```ts
118
+ import './untodo.config';
119
+ ```
120
+
121
+ ### 2. `global.d.ts` — extending the meta types
122
+
123
+ `TODO()`, `FIXME()`, and `HACK()` accept structured metadata. By default only `reason: string` is required. Add project-specific fields to the `TodoMeta` / `FixmeMeta` / `HackMeta` interfaces via TypeScript [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).
124
+
125
+ The generated template:
126
+
127
+ ```ts
128
+ declare module 'untodo' {
129
+ interface TodoMeta {
130
+ // issue?: number | string;
131
+ // assignee?: string;
132
+ }
133
+ interface FixmeMeta {
134
+ // issue?: number | string;
135
+ }
136
+ interface HackMeta {
137
+ // issue?: number | string;
138
+ }
139
+ }
140
+
141
+ export {};
142
+ ```
143
+
144
+ Uncomment or add fields to fit your team's workflow. For example:
47
145
 
48
146
  ```ts
49
- // eslint.config.ts
50
- import untodoPlugin from "untodo/eslint";
147
+ declare module 'untodo' {
148
+ interface TodoMeta {
149
+ issue?: number | string;
150
+ assignee?: string;
151
+ severity?: 'low' | 'medium' | 'high';
152
+ }
153
+ interface FixmeMeta {
154
+ issue?: number | string;
155
+ }
156
+ interface HackMeta {
157
+ issue?: number | string;
158
+ }
159
+ }
160
+
161
+ export {};
162
+ ```
163
+
164
+ After this, callers get full type checking and IDE autocomplete for the extra fields:
165
+
166
+ ```ts
167
+ return TODO({
168
+ reason: 'wire up the user repository',
169
+ issue: 123, // ← typed
170
+ assignee: 'alice', // ← typed
171
+ severity: 'high', // ← typed, only 'low' | 'medium' | 'high' allowed
172
+ });
173
+ ```
174
+
175
+ Make sure `global.d.ts` is included by your `tsconfig.json` (most default `"include"` patterns already pick up `**/*.d.ts`).
176
+
177
+ > The trailing `export {};` keeps the file as a module, which is required for `declare module` augmentation to apply.
178
+
179
+ ### 3. Add the ESLint plugin
180
+
181
+ `init` finishes by printing a flat-config snippet. Paste it into your existing `eslint.config.ts` (or `.mjs`):
182
+
183
+ ```ts
184
+ import untodoPlugin from 'untodo/eslint';
51
185
 
52
186
  export default [
53
187
  {
54
188
  plugins: { untodo: untodoPlugin },
55
189
  rules: {
56
- "untodo/no-todo": "error",
57
- "untodo/no-fixme": "error",
58
- "untodo/no-hack": "warn",
190
+ 'untodo/no-todo': 'error',
191
+ 'untodo/no-fixme': 'error',
192
+ 'untodo/no-hack': 'warn',
59
193
  },
60
194
  },
61
195
  ];
@@ -63,6 +197,83 @@ export default [
63
197
 
64
198
  The same plugin works with [oxlint](https://oxc.rs/docs/guide/usage/linter.html)'s JS plugin support.
65
199
 
200
+ ## Usage
201
+
202
+ ```ts
203
+ import { TODO, FIXME, HACK } from 'untodo';
204
+
205
+ function fetchUser(): User {
206
+ return TODO({ reason: 'wire up the user repository' });
207
+ }
208
+
209
+ function parseDate(input: string): Date {
210
+ return FIXME({ reason: 'rejects valid ISO strings with offsets' });
211
+ }
212
+
213
+ function legacyAdapter(): Adapter {
214
+ return HACK({ reason: 'old API ships in the next major; remove then' });
215
+ }
216
+ ```
217
+
218
+ Because each function returns `never`, the surrounding code keeps type-checking as if a real value flowed through — there's no need to add `as User` casts or fake return values.
219
+
220
+ ### Per-call handlers
221
+
222
+ Pass a callback as the second argument to override the global handler for one call:
223
+
224
+ ```ts
225
+ return TODO(
226
+ { reason: 'wire up the user repository' },
227
+ (meta) => myLogger.warn('todo.hit', meta),
228
+ );
229
+ ```
230
+
231
+ ### Opting out of the global handler
232
+
233
+ Once `defineConfig({ onTodo })` is set, **every** `TODO()` call without a per-call callback fires the global handler. That's usually what you want — but on a hot path, or for a deliberately quiet placeholder, you may want one specific call to skip the handler.
234
+
235
+ `untodo` does not bake an opt-out flag into the meta types — naming and semantics are policy, and policy belongs to your team. The recommended pattern is to add a boolean field via Declaration Merging and short-circuit inside your handler:
236
+
237
+ ```ts
238
+ // global.d.ts — pick whatever name your team prefers (silent / muted / skip ...)
239
+ declare module 'untodo' {
240
+ interface TodoMeta {
241
+ silent?: boolean;
242
+ }
243
+ }
244
+
245
+ // untodo.config.ts
246
+ import { defineConfig } from 'untodo';
247
+
248
+ export default defineConfig({
249
+ onTodo: (meta) => {
250
+ if (meta.silent) return;
251
+ sendToSentry(meta);
252
+ },
253
+ });
254
+ ```
255
+
256
+ ```ts
257
+ // usage
258
+ TODO({ reason: 'normal placeholder' }); // global fires
259
+ TODO({ reason: 'tight loop', silent: true }); // global skipped
260
+ TODO({ reason: 'custom path' }, (m) => myLogger.warn(m)); // per-call wins, global skipped
261
+ ```
262
+
263
+ The same pattern applies to `FixmeMeta` / `HackMeta`. Keeping the gate inside the handler — rather than as a library-level switch — means the predicate can be as nuanced as you need (`if (meta.severity === 'low' && process.env.CI) return;` etc.) without `untodo` having to anticipate every shape.
264
+
265
+ ## API
266
+
267
+ | Export | Description |
268
+ |---|---|
269
+ | `TODO(meta, cb?)` | Marks a code path as not yet implemented. Returns `never`. |
270
+ | `FIXME(meta, cb?)` | Marks broken code that must be repaired. Returns `never`. |
271
+ | `HACK(meta, cb?)` | Marks a deliberate workaround to revisit. Returns `never`. |
272
+ | `defineConfig(config)` | Registers project-wide defaults (used in `untodo.config.ts`). |
273
+ | `untodo/eslint` | ESLint / oxlint plugin exposing `no-todo`, `no-fixme`, `no-hack` rules. |
274
+
275
+ None of `TODO` / `FIXME` / `HACK` throw at runtime — they only invoke the configured handler. Lint enforcement is what makes them block the build.
276
+
66
277
  ## Scripts
67
278
 
68
279
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "untodo",
3
- "version": "0.0.1-alpha.0",
4
- "description": "Type-safe replacement for TODO/FIXME/HACK comments — trackable in IDE, enforceable by lint and the type system.",
3
+ "version": "0.0.1",
4
+ "description": "Type-safe TODO for humans and AI — trackable, structured, and lint-enforceable.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.mjs",
@@ -66,7 +66,7 @@
66
66
  "vitest": "^2.1.8"
67
67
  },
68
68
  "peerDependencies": {
69
- "eslint": "^8.0.0 || ^9.0.0"
69
+ "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
70
70
  },
71
71
  "peerDependenciesMeta": {
72
72
  "eslint": {