@mono-labs/tracker 0.1.269
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.
- package/README.md +1196 -0
- package/bin/tracker.js +2 -0
- package/dist/dashboard/cli.d.ts +2 -0
- package/dist/dashboard/cli.d.ts.map +1 -0
- package/dist/dashboard/cli.js +38 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +5 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +130 -0
- package/dist/dashboard/types.d.ts +13 -0
- package/dist/dashboard/types.d.ts.map +1 -0
- package/dist/dashboard/types.js +2 -0
- package/dist/dashboard/watcher.d.ts +7 -0
- package/dist/dashboard/watcher.d.ts.map +1 -0
- package/dist/dashboard/watcher.js +44 -0
- package/dist/executor/action-executor.d.ts +10 -0
- package/dist/executor/action-executor.d.ts.map +1 -0
- package/dist/executor/action-executor.js +19 -0
- package/dist/executor/actions/index.d.ts +4 -0
- package/dist/executor/actions/index.d.ts.map +1 -0
- package/dist/executor/actions/index.js +9 -0
- package/dist/executor/actions/remove-action.d.ts +4 -0
- package/dist/executor/actions/remove-action.d.ts.map +1 -0
- package/dist/executor/actions/remove-action.js +10 -0
- package/dist/executor/actions/rename-action.d.ts +4 -0
- package/dist/executor/actions/rename-action.d.ts.map +1 -0
- package/dist/executor/actions/rename-action.js +10 -0
- package/dist/executor/actions/replace-action.d.ts +4 -0
- package/dist/executor/actions/replace-action.d.ts.map +1 -0
- package/dist/executor/actions/replace-action.js +10 -0
- package/dist/executor/index.d.ts +4 -0
- package/dist/executor/index.d.ts.map +1 -0
- package/dist/executor/index.js +10 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/manager/index.d.ts +7 -0
- package/dist/manager/index.d.ts.map +1 -0
- package/dist/manager/index.js +19 -0
- package/dist/manager/notation-manager.d.ts +18 -0
- package/dist/manager/notation-manager.d.ts.map +1 -0
- package/dist/manager/notation-manager.js +137 -0
- package/dist/manager/notation-manager.test.d.ts +2 -0
- package/dist/manager/notation-manager.test.d.ts.map +1 -0
- package/dist/manager/notation-manager.test.js +211 -0
- package/dist/manager/notation-updater.d.ts +6 -0
- package/dist/manager/notation-updater.d.ts.map +1 -0
- package/dist/manager/notation-updater.js +20 -0
- package/dist/manager/relationship-manager.d.ts +5 -0
- package/dist/manager/relationship-manager.d.ts.map +1 -0
- package/dist/manager/relationship-manager.js +46 -0
- package/dist/manager/stats.d.ts +3 -0
- package/dist/manager/stats.d.ts.map +1 -0
- package/dist/manager/stats.js +41 -0
- package/dist/manager/validator.d.ts +9 -0
- package/dist/manager/validator.d.ts.map +1 -0
- package/dist/manager/validator.js +62 -0
- package/dist/scanner/action-parser.d.ts +3 -0
- package/dist/scanner/action-parser.d.ts.map +1 -0
- package/dist/scanner/action-parser.js +97 -0
- package/dist/scanner/action-parser.test.d.ts +2 -0
- package/dist/scanner/action-parser.test.d.ts.map +1 -0
- package/dist/scanner/action-parser.test.js +94 -0
- package/dist/scanner/attribute-parser.d.ts +15 -0
- package/dist/scanner/attribute-parser.d.ts.map +1 -0
- package/dist/scanner/attribute-parser.js +183 -0
- package/dist/scanner/attribute-parser.test.d.ts +2 -0
- package/dist/scanner/attribute-parser.test.d.ts.map +1 -0
- package/dist/scanner/attribute-parser.test.js +93 -0
- package/dist/scanner/file-scanner.d.ts +3 -0
- package/dist/scanner/file-scanner.d.ts.map +1 -0
- package/dist/scanner/file-scanner.js +58 -0
- package/dist/scanner/index.d.ts +6 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +11 -0
- package/dist/scanner/notation-parser.d.ts +3 -0
- package/dist/scanner/notation-parser.d.ts.map +1 -0
- package/dist/scanner/notation-parser.js +88 -0
- package/dist/scanner/notation-parser.test.d.ts +2 -0
- package/dist/scanner/notation-parser.test.d.ts.map +1 -0
- package/dist/scanner/notation-parser.test.js +153 -0
- package/dist/storage/config-loader.d.ts +3 -0
- package/dist/storage/config-loader.d.ts.map +1 -0
- package/dist/storage/config-loader.js +55 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/jsonl-storage.d.ts +11 -0
- package/dist/storage/jsonl-storage.d.ts.map +1 -0
- package/dist/storage/jsonl-storage.js +88 -0
- package/dist/storage/jsonl-storage.test.d.ts +2 -0
- package/dist/storage/jsonl-storage.test.d.ts.map +1 -0
- package/dist/storage/jsonl-storage.test.js +126 -0
- package/dist/types/action.d.ts +57 -0
- package/dist/types/action.d.ts.map +1 -0
- package/dist/types/action.js +13 -0
- package/dist/types/config.d.ts +11 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +11 -0
- package/dist/types/enums.d.ts +40 -0
- package/dist/types/enums.d.ts.map +1 -0
- package/dist/types/enums.js +37 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/notation.d.ts +64 -0
- package/dist/types/notation.d.ts.map +1 -0
- package/dist/types/notation.js +2 -0
- package/dist/utils/date-parser.d.ts +3 -0
- package/dist/utils/date-parser.d.ts.map +1 -0
- package/dist/utils/date-parser.js +51 -0
- package/dist/utils/date-parser.test.d.ts +2 -0
- package/dist/utils/date-parser.test.d.ts.map +1 -0
- package/dist/utils/date-parser.test.js +34 -0
- package/dist/utils/id-generator.d.ts +3 -0
- package/dist/utils/id-generator.d.ts.map +1 -0
- package/dist/utils/id-generator.js +12 -0
- package/dist/utils/id-generator.test.d.ts +2 -0
- package/dist/utils/id-generator.test.d.ts.map +1 -0
- package/dist/utils/id-generator.test.js +30 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
# @mono-labs/tracker
|
|
2
|
+
|
|
3
|
+
Code notation tracker for scanning, parsing, and managing structured comment markers across your codebase.
|
|
4
|
+
|
|
5
|
+
<!-- Badges -->
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [What is Tracker?](#what-is-tracker)
|
|
16
|
+
- [Notation Syntax Guide](#notation-syntax-guide)
|
|
17
|
+
- [Basic Syntax](#basic-syntax)
|
|
18
|
+
- [Inline IDs](#inline-ids)
|
|
19
|
+
- [Multi-line Notations](#multi-line-notations)
|
|
20
|
+
- [Code Context Capture](#code-context-capture)
|
|
21
|
+
- [Attribute Styles](#attribute-styles)
|
|
22
|
+
- [Actions](#actions)
|
|
23
|
+
- [Relationships](#relationships)
|
|
24
|
+
- [Performance Impact](#performance-impact)
|
|
25
|
+
- [Technical Debt](#technical-debt)
|
|
26
|
+
- [Priority Shorthand](#priority-shorthand)
|
|
27
|
+
- [Risk Shorthand](#risk-shorthand)
|
|
28
|
+
- [Configuration](#configuration)
|
|
29
|
+
- [Core API Walkthrough](#core-api-walkthrough)
|
|
30
|
+
- [Scanning Files](#scanning-files)
|
|
31
|
+
- [NotationManager](#notationmanager)
|
|
32
|
+
- [Querying](#querying)
|
|
33
|
+
- [Storage](#storage)
|
|
34
|
+
- [Utilities](#utilities)
|
|
35
|
+
- [Relationships & Validation](#relationships--validation)
|
|
36
|
+
- [Mutation Helpers](#mutation-helpers)
|
|
37
|
+
- [Executor Framework](#executor-framework)
|
|
38
|
+
- [Full Type Reference](#full-type-reference)
|
|
39
|
+
- [Architecture](#architecture)
|
|
40
|
+
- [Contributor Guide](#contributor-guide)
|
|
41
|
+
- [License](#license)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
Install the package:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
yarn add @mono-labs/tracker
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Scan your project and view results in three steps:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { loadConfig, scanFiles, NotationManager } from '@mono-labs/tracker'
|
|
57
|
+
|
|
58
|
+
// 1. Load config (reads tracker.config.json or uses defaults)
|
|
59
|
+
const config = loadConfig(process.cwd())
|
|
60
|
+
|
|
61
|
+
// 2. Scan source files for notations
|
|
62
|
+
const notations = await scanFiles(config)
|
|
63
|
+
|
|
64
|
+
// 3. Manage and query results
|
|
65
|
+
const manager = new NotationManager(config)
|
|
66
|
+
manager.setAll(notations)
|
|
67
|
+
await manager.save()
|
|
68
|
+
|
|
69
|
+
console.log(manager.stats())
|
|
70
|
+
console.log(manager.query({ type: 'TODO', priority: 'high' }))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## What is Tracker?
|
|
76
|
+
|
|
77
|
+
Codebases accumulate structured comments — `TODO`, `FIXME`, `BUG`, `HACK`, `NOTE`, `OPTIMIZE`, `SECURITY` — scattered across hundreds of files. These carry intent, ownership, deadlines, and technical debt information, but they're invisible to your tooling.
|
|
78
|
+
|
|
79
|
+
**Tracker** solves this by providing a pipeline to:
|
|
80
|
+
|
|
81
|
+
1. **Scan** — Discover notation comments via configurable glob patterns
|
|
82
|
+
2. **Parse** — Extract structured data: priority, assignee, tags, due dates, actions, relationships, performance impact, and technical debt
|
|
83
|
+
3. **Persist** — Store results in append-friendly JSONL format with atomic writes
|
|
84
|
+
4. **Query** — Filter notations by any combination of fields
|
|
85
|
+
5. **Validate** — Check for missing fields, duplicate IDs, broken references, and circular dependencies
|
|
86
|
+
6. **Report** — Compute aggregate statistics across your notations
|
|
87
|
+
7. **Execute** — Dispatch parsed actions to registered handler functions
|
|
88
|
+
|
|
89
|
+
### Supported Marker Types
|
|
90
|
+
|
|
91
|
+
| Marker | Purpose |
|
|
92
|
+
|------------|--------------------------------------|
|
|
93
|
+
| `TODO` | Planned work |
|
|
94
|
+
| `FIXME` | Known issue needing a fix |
|
|
95
|
+
| `BUG` | Confirmed defect |
|
|
96
|
+
| `HACK` | Temporary workaround |
|
|
97
|
+
| `NOTE` | Informational annotation |
|
|
98
|
+
| `OPTIMIZE` | Performance improvement opportunity |
|
|
99
|
+
| `SECURITY` | Security-related concern |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Notation Syntax Guide
|
|
104
|
+
|
|
105
|
+
### Basic Syntax
|
|
106
|
+
|
|
107
|
+
Single-line notation with a marker and description:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// TODO: Refactor this function to use async/await
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The colon after the marker is optional:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// FIXME Broken on empty input
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Inline IDs
|
|
120
|
+
|
|
121
|
+
Assign a stable external ID using square brackets:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// TODO [TASK-123] Migrate to the new API
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When no inline ID is provided, Tracker generates a deterministic ID from the file path and line number using SHA-256.
|
|
128
|
+
|
|
129
|
+
### Multi-line Notations
|
|
130
|
+
|
|
131
|
+
Continuation comment lines immediately following a marker are collected as the notation body:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// TODO: Refactor authentication module
|
|
135
|
+
// This function has grown too complex and handles
|
|
136
|
+
// both session management and token refresh.
|
|
137
|
+
// @author: Alice
|
|
138
|
+
// @priority: high
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Continuation stops at blank lines, non-comment lines, or a new marker.
|
|
142
|
+
|
|
143
|
+
### Code Context Capture
|
|
144
|
+
|
|
145
|
+
Non-comment, non-empty lines immediately following the notation block are captured as code context:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// TODO: This query is too slow
|
|
149
|
+
// @priority: high
|
|
150
|
+
// Performance: 2000ms->100ms
|
|
151
|
+
const results = db.query('SELECT * FROM users')
|
|
152
|
+
const filtered = results.filter(u => u.active)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Here `results` and `filtered` lines are captured in the `codeContext` array.
|
|
156
|
+
|
|
157
|
+
### Attribute Styles
|
|
158
|
+
|
|
159
|
+
Tracker supports three attribute formats. All can be mixed within the same notation body.
|
|
160
|
+
|
|
161
|
+
#### 1. `@` Prefix Style
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// TODO: Implement caching layer
|
|
165
|
+
// @author: Alice
|
|
166
|
+
// @assignee: Bob
|
|
167
|
+
// @priority: high
|
|
168
|
+
// @tags: performance, api
|
|
169
|
+
// @due: +2w
|
|
170
|
+
// @risk: moderate
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### 2. Compact Bracket Style
|
|
174
|
+
|
|
175
|
+
Pack multiple attributes into a single bracketed line. Segments are separated by `|`.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// TODO: Fix login redirect
|
|
179
|
+
// [Alice → Bob | high | 3d | due: 2/24/2026 | tags: auth, ui]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- **Arrow assignment** (`→` or `->`): sets `author` and `assignee`
|
|
183
|
+
- **Duration shorthand** (`3d`, `2w`, `1m`): sets due date relative to today (hours like `8h` set debt instead)
|
|
184
|
+
- **Key-value** (`due: 2/24/2026`): same as `@` prefix keys
|
|
185
|
+
- **Bare words** (`high`, `critical`): matched against priority/risk maps
|
|
186
|
+
|
|
187
|
+
#### 3. Key-Value Style
|
|
188
|
+
|
|
189
|
+
Capitalized key followed by colon and value:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// TODO: Update error handling
|
|
193
|
+
// Priority: critical
|
|
194
|
+
// Tags: ui, api
|
|
195
|
+
// Assignee: Charlie
|
|
196
|
+
// Debt: 8h | compounding: high
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Actions
|
|
200
|
+
|
|
201
|
+
Declare code transformation intentions with `Action:` lines:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// TODO: Clean up legacy code
|
|
205
|
+
// Action: replace(oldFunction, newFunction)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Chained calls for positional actions:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// TODO: Add error boundary
|
|
212
|
+
// Action: insert(ErrorBoundary).before(App)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Supported action verbs: `replace`, `remove`, `rename`, `insert`, `extract`, `move`, `wrapIn`. Unrecognized verbs are parsed as `generic`.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// Action: remove(legacyHelper)
|
|
219
|
+
// Action: rename(fetchData, loadData)
|
|
220
|
+
// Action: extract(validateInput).to(validators.ts)
|
|
221
|
+
// Action: move(utils).to(shared/utils.ts)
|
|
222
|
+
// Action: wrapIn(rawQuery, sanitize)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Relationships
|
|
226
|
+
|
|
227
|
+
Declare dependencies between notations:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// TODO: Implement logout
|
|
231
|
+
// Blocks: N-abc12345
|
|
232
|
+
// Depends on: N-def45678
|
|
233
|
+
// Related: N-ghi78901, N-jkl01234
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Relationship keys: `Blocks`, `Blocked by`, `Depends on`, `Related`. All populate the `relationships` array with referenced IDs. Notation is considered _blocked_ if any related notation has a non-resolved status.
|
|
237
|
+
|
|
238
|
+
### Performance Impact
|
|
239
|
+
|
|
240
|
+
Track measured or expected performance changes:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// OPTIMIZE: Replace N+1 query with batch load
|
|
244
|
+
// Performance: 2000ms->100ms
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Parsed into a `PerformanceImpact` object with `before`, `after`, and `unit` fields. Supported units: `ms`, `s`, `us`.
|
|
248
|
+
|
|
249
|
+
### Technical Debt
|
|
250
|
+
|
|
251
|
+
Estimate and track accumulated debt:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
// HACK: Hardcoded timeout
|
|
255
|
+
// Debt: 8h | compounding: high
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The hours value must use the `h` suffix. Compounding rate is `low`, `medium`, or `high` (defaults to `low`).
|
|
259
|
+
|
|
260
|
+
### Priority Shorthand
|
|
261
|
+
|
|
262
|
+
| Shorthand | Full Value |
|
|
263
|
+
|-----------|-----------|
|
|
264
|
+
| `m1` | `minimal` |
|
|
265
|
+
| `l2` | `low` |
|
|
266
|
+
| `m3`, `med` | `medium` |
|
|
267
|
+
| `h4` | `high` |
|
|
268
|
+
| `c5` | `critical`|
|
|
269
|
+
|
|
270
|
+
### Risk Shorthand
|
|
271
|
+
|
|
272
|
+
| Shorthand | Full Value |
|
|
273
|
+
|-----------|------------|
|
|
274
|
+
| `m1` | `minimal` |
|
|
275
|
+
| `l2` | `low` |
|
|
276
|
+
| `m3`, `mod` | `moderate` |
|
|
277
|
+
| `s2` | `severe` |
|
|
278
|
+
| `c3` | `critical` |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Configuration
|
|
283
|
+
|
|
284
|
+
Create a `tracker.config.json` in your project root:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
289
|
+
"exclude": ["**/node_modules/**", "**/dist/**", "**/*.test.ts"],
|
|
290
|
+
"markers": ["TODO", "FIXME", "BUG"],
|
|
291
|
+
"storagePath": ".tracker/notations.jsonl",
|
|
292
|
+
"idPrefix": "N"
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Default Configuration
|
|
297
|
+
|
|
298
|
+
If no config file is found, or for any omitted fields, these defaults apply:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const DEFAULT_CONFIG: TrackerConfig = {
|
|
302
|
+
rootDir: '.',
|
|
303
|
+
include: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
304
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
|
|
305
|
+
markers: ['TODO', 'FIXME', 'BUG', 'HACK', 'NOTE', 'OPTIMIZE', 'SECURITY'],
|
|
306
|
+
storagePath: '.tracker/notations.jsonl',
|
|
307
|
+
idPrefix: 'N',
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### `loadConfig(projectRoot: string): TrackerConfig`
|
|
312
|
+
|
|
313
|
+
Reads `tracker.config.json` from `projectRoot`, merges with `DEFAULT_CONFIG`, and sets `rootDir` to `projectRoot`. If the file is missing or contains invalid JSON, defaults are used silently.
|
|
314
|
+
|
|
315
|
+
### `TrackerConfig` Interface
|
|
316
|
+
|
|
317
|
+
| Field | Type | Description |
|
|
318
|
+
|---------------|--------------|--------------------------------------------------|
|
|
319
|
+
| `rootDir` | `string` | Absolute root directory (set by `loadConfig`) |
|
|
320
|
+
| `include` | `string[]` | Glob patterns for files to scan |
|
|
321
|
+
| `exclude` | `string[]` | Glob patterns for files to skip |
|
|
322
|
+
| `markers` | `MarkerType[]` | Which marker types to recognize |
|
|
323
|
+
| `storagePath` | `string` | Path to the JSONL storage file (relative to root) |
|
|
324
|
+
| `idPrefix` | `string` | Prefix for generated notation IDs |
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Core API Walkthrough
|
|
329
|
+
|
|
330
|
+
### Scanning Files
|
|
331
|
+
|
|
332
|
+
#### `scanFiles(config: TrackerConfig, rootDir?: string): Promise<Notation[]>`
|
|
333
|
+
|
|
334
|
+
Discovers files matching `config.include` (excluding `config.exclude`) using [fast-glob](https://github.com/mrmlnc/fast-glob), reads each file, and parses all notations.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { loadConfig, scanFiles } from '@mono-labs/tracker'
|
|
338
|
+
|
|
339
|
+
const config = loadConfig('/path/to/project')
|
|
340
|
+
const notations = await scanFiles(config)
|
|
341
|
+
console.log(`Found ${notations.length} notations`)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The optional `rootDir` parameter overrides `config.rootDir` for the scan.
|
|
345
|
+
|
|
346
|
+
#### `parseFileContent(filePath: string, content: string, idPrefix?: string): Notation[]`
|
|
347
|
+
|
|
348
|
+
Parses notation markers from a raw file string. Useful when you already have file content in memory.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { parseFileContent } from '@mono-labs/tracker'
|
|
352
|
+
|
|
353
|
+
const source = `
|
|
354
|
+
// TODO: Implement validation
|
|
355
|
+
// @priority: high
|
|
356
|
+
function validate() {}
|
|
357
|
+
`
|
|
358
|
+
|
|
359
|
+
const notations = parseFileContent('src/validate.ts', source, 'N')
|
|
360
|
+
// notations[0].description === 'Implement validation'
|
|
361
|
+
// notations[0].priority === 'high'
|
|
362
|
+
// notations[0].codeContext === ['function validate() {}']
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### `parseAttributes(bodyLines: string[]): ParsedAttributes`
|
|
366
|
+
|
|
367
|
+
Parses attribute lines from a notation body independently. Returns a `ParsedAttributes` object.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { parseAttributes } from '@mono-labs/tracker'
|
|
371
|
+
|
|
372
|
+
const attrs = parseAttributes([
|
|
373
|
+
'@author: Alice',
|
|
374
|
+
'@priority: high',
|
|
375
|
+
'@tags: ui, perf',
|
|
376
|
+
])
|
|
377
|
+
// attrs.author === 'Alice'
|
|
378
|
+
// attrs.priority === 'high'
|
|
379
|
+
// attrs.tags === ['ui', 'perf']
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### `parseActions(bodyLines: string[]): NotationAction[]`
|
|
383
|
+
|
|
384
|
+
Parses `Action:` lines from a notation body independently.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { parseActions } from '@mono-labs/tracker'
|
|
388
|
+
|
|
389
|
+
const actions = parseActions([
|
|
390
|
+
'Action: replace(oldFn, newFn)',
|
|
391
|
+
'Action: insert(guard).before(handler)',
|
|
392
|
+
])
|
|
393
|
+
// actions[0].args.verb === 'replace'
|
|
394
|
+
// actions[1].args.verb === 'insert'
|
|
395
|
+
// actions[1].args.position === 'before'
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### NotationManager
|
|
399
|
+
|
|
400
|
+
The `NotationManager` class is the primary facade for loading, querying, mutating, and persisting notations.
|
|
401
|
+
|
|
402
|
+
#### Constructor
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { NotationManager, loadConfig } from '@mono-labs/tracker'
|
|
406
|
+
|
|
407
|
+
const config = loadConfig(process.cwd())
|
|
408
|
+
const manager = new NotationManager(config)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The storage file path is resolved from `config.storagePath` (relative paths resolve against `config.rootDir`).
|
|
412
|
+
|
|
413
|
+
#### `load(): Promise<void>`
|
|
414
|
+
|
|
415
|
+
Reads all notations from the JSONL storage file into memory.
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
await manager.load()
|
|
419
|
+
console.log(manager.getAll().length)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### `save(): Promise<void>`
|
|
423
|
+
|
|
424
|
+
Writes all in-memory notations to the JSONL storage file (atomic write via tmp + rename).
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
manager.setAll(notations)
|
|
428
|
+
await manager.save()
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### `getAll(): Notation[]`
|
|
432
|
+
|
|
433
|
+
Returns a shallow copy of all notations currently in memory.
|
|
434
|
+
|
|
435
|
+
#### `getById(id: string): Notation | undefined`
|
|
436
|
+
|
|
437
|
+
Returns a single notation by its ID, or `undefined` if not found.
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
const notation = manager.getById('N-abc12345')
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
#### `setAll(notations: Notation[]): void`
|
|
444
|
+
|
|
445
|
+
Replaces all in-memory notations with a new array (shallow copy).
|
|
446
|
+
|
|
447
|
+
#### `query(q: NotationQuery): Notation[]`
|
|
448
|
+
|
|
449
|
+
Filters notations by any combination of query fields. See [Querying](#querying) for full filter reference.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
const critical = manager.query({ priority: 'critical', status: 'open' })
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### `update(id: string, updates: Partial<Notation>): boolean`
|
|
456
|
+
|
|
457
|
+
Merges `updates` into the notation with the given ID. Returns `true` if the notation was found and updated, `false` otherwise.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
manager.update('N-abc12345', { status: 'resolved', assignee: 'Bob' })
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### `validate(): ValidationError[]`
|
|
464
|
+
|
|
465
|
+
Runs full validation across all notations: field checks, duplicate IDs, broken references, and circular dependency detection.
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
const errors = manager.validate()
|
|
469
|
+
if (errors.length > 0) {
|
|
470
|
+
console.error('Validation errors:', errors)
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### `stats(): NotationStats`
|
|
475
|
+
|
|
476
|
+
Computes aggregate statistics over all in-memory notations.
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
const s = manager.stats()
|
|
480
|
+
console.log(`Total: ${s.total}, Overdue: ${s.overdue}, Debt: ${s.totalDebtHours}h`)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Querying
|
|
484
|
+
|
|
485
|
+
The `query()` method accepts a `NotationQuery` object. All fields are optional; multiple fields combine with AND logic.
|
|
486
|
+
|
|
487
|
+
| Field | Type | Behavior |
|
|
488
|
+
|-------------|--------------------------------|-------------------------------------------------------------|
|
|
489
|
+
| `type` | `MarkerType \| MarkerType[]` | Match one or more marker types |
|
|
490
|
+
| `tags` | `string[]` | Match notations containing _any_ of the specified tags |
|
|
491
|
+
| `priority` | `Priority \| Priority[]` | Match one or more priority levels |
|
|
492
|
+
| `status` | `Status \| Status[]` | Match one or more statuses |
|
|
493
|
+
| `file` | `string` | Substring match against `location.file` |
|
|
494
|
+
| `assignee` | `string` | Exact match against `assignee` |
|
|
495
|
+
| `overdue` | `boolean` | If `true`, return only overdue non-resolved notations |
|
|
496
|
+
| `blocked` | `boolean` | If `true`, return only blocked notations |
|
|
497
|
+
| `search` | `string` | Case-insensitive substring search in description, body, tags|
|
|
498
|
+
| `dueBefore` | `string` | ISO date string — notations due on or before this date |
|
|
499
|
+
| `dueAfter` | `string` | ISO date string — notations due on or after this date |
|
|
500
|
+
|
|
501
|
+
Array values enable multi-select filtering:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
manager.query({ type: ['TODO', 'BUG'], priority: ['high', 'critical'] })
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Storage
|
|
508
|
+
|
|
509
|
+
#### `JsonlStorage`
|
|
510
|
+
|
|
511
|
+
Low-level storage engine that persists notations as newline-delimited JSON.
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
import { JsonlStorage } from '@mono-labs/tracker'
|
|
515
|
+
|
|
516
|
+
const storage = new JsonlStorage('.tracker/notations.jsonl')
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
##### `readAll(): Promise<Notation[]>`
|
|
520
|
+
|
|
521
|
+
Reads and parses all lines from the JSONL file. Corrupt lines are silently skipped. Returns an empty array if the file doesn't exist.
|
|
522
|
+
|
|
523
|
+
##### `writeAll(notations: Notation[]): Promise<void>`
|
|
524
|
+
|
|
525
|
+
Atomically writes all notations: writes to a `.tmp` file first, then renames over the target. Creates parent directories if needed.
|
|
526
|
+
|
|
527
|
+
##### `append(notation: Notation): Promise<void>`
|
|
528
|
+
|
|
529
|
+
Appends a single notation as a new line to the file.
|
|
530
|
+
|
|
531
|
+
##### `appendBatch(notations: Notation[]): Promise<void>`
|
|
532
|
+
|
|
533
|
+
Appends multiple notations in a single write operation. No-ops on empty arrays.
|
|
534
|
+
|
|
535
|
+
### Utilities
|
|
536
|
+
|
|
537
|
+
#### `generateId(prefix?: string): string`
|
|
538
|
+
|
|
539
|
+
Generates a random ID using UUID v4. Default prefix is `'N'`.
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { generateId } from '@mono-labs/tracker'
|
|
543
|
+
|
|
544
|
+
generateId() // 'N-a1b2c3d4'
|
|
545
|
+
generateId('T') // 'T-e5f6a7b8'
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
#### `generateStableId(prefix: string, file: string, line: number): string`
|
|
549
|
+
|
|
550
|
+
Generates a deterministic ID from a file path and line number using SHA-256. Re-scanning the same file produces the same IDs, enabling incremental updates.
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
import { generateStableId } from '@mono-labs/tracker'
|
|
554
|
+
|
|
555
|
+
generateStableId('N', 'src/app.ts', 42) // 'N-<8-char hash>'
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### `parseDate(input: string): string | null`
|
|
559
|
+
|
|
560
|
+
Parses date strings in multiple formats. Returns an ISO date string (`YYYY-MM-DD`) or `null`.
|
|
561
|
+
|
|
562
|
+
| Format | Example | Notes |
|
|
563
|
+
|------------------|----------------|--------------------------------|
|
|
564
|
+
| ISO | `2026-02-24` | Returned as-is |
|
|
565
|
+
| US | `2/24/2026` | `MM/DD/YYYY`, zero-padding optional |
|
|
566
|
+
| Relative days | `+3d` | 3 days from today |
|
|
567
|
+
| Relative weeks | `+2w` | 14 days from today |
|
|
568
|
+
| Relative months | `+1m` | 1 month from today |
|
|
569
|
+
| Relative years | `+1y` | 1 year from today |
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
import { parseDate } from '@mono-labs/tracker'
|
|
573
|
+
|
|
574
|
+
parseDate('2026-02-24') // '2026-02-24'
|
|
575
|
+
parseDate('2/24/2026') // '2026-02-24'
|
|
576
|
+
parseDate('+2w') // ISO date 14 days from now
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
#### `isOverdue(dateStr: string): boolean`
|
|
580
|
+
|
|
581
|
+
Returns `true` if the given ISO date string is in the past (compared at end of day, 23:59:59).
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
import { isOverdue } from '@mono-labs/tracker'
|
|
585
|
+
|
|
586
|
+
isOverdue('2020-01-01') // true
|
|
587
|
+
isOverdue('2099-12-31') // false
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Relationships & Validation
|
|
591
|
+
|
|
592
|
+
#### `getBlockers(notation: Notation, allNotations: Notation[]): Notation[]`
|
|
593
|
+
|
|
594
|
+
Returns all notations referenced in `notation.relationships` that have a non-resolved status.
|
|
595
|
+
|
|
596
|
+
#### `isBlocked(notation: Notation, allNotations: Notation[]): boolean`
|
|
597
|
+
|
|
598
|
+
Returns `true` if the notation has any unresolved blockers.
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import { isBlocked, getBlockers } from '@mono-labs/tracker'
|
|
602
|
+
|
|
603
|
+
const blockers = getBlockers(myNotation, allNotations)
|
|
604
|
+
if (isBlocked(myNotation, allNotations)) {
|
|
605
|
+
console.log('Blocked by:', blockers.map(b => b.id))
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### `detectCircularDependencies(notations: Notation[]): string[][]`
|
|
610
|
+
|
|
611
|
+
Uses DFS to detect cycles in the relationship graph. Returns an array of cycles, where each cycle is an array of notation IDs forming the loop.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { detectCircularDependencies } from '@mono-labs/tracker'
|
|
615
|
+
|
|
616
|
+
const cycles = detectCircularDependencies(allNotations)
|
|
617
|
+
// [['N-abc', 'N-def', 'N-abc']]
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
#### `validateNotation(notation: Notation): ValidationError[]`
|
|
621
|
+
|
|
622
|
+
Validates a single notation for:
|
|
623
|
+
- Missing `id`, `description`, or `location.file`
|
|
624
|
+
- Invalid `type`, `status`, `priority`, or `risk` values
|
|
625
|
+
|
|
626
|
+
#### `validateAll(notations: Notation[]): ValidationError[]`
|
|
627
|
+
|
|
628
|
+
Validates all notations and additionally checks for:
|
|
629
|
+
- Duplicate IDs
|
|
630
|
+
- Broken relationship references (IDs not in the set)
|
|
631
|
+
- Circular dependencies
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
import { validateAll } from '@mono-labs/tracker'
|
|
635
|
+
|
|
636
|
+
const errors = validateAll(notations)
|
|
637
|
+
for (const err of errors) {
|
|
638
|
+
console.error(`${err.notationId} [${err.field}]: ${err.message}`)
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
#### `computeStats(notations: Notation[]): NotationStats`
|
|
643
|
+
|
|
644
|
+
Standalone function to compute statistics from a notation array (same logic as `manager.stats()`).
|
|
645
|
+
|
|
646
|
+
### Mutation Helpers
|
|
647
|
+
|
|
648
|
+
Immutable helper functions that return new `Notation` objects:
|
|
649
|
+
|
|
650
|
+
#### `updateStatus(notation: Notation, status: Status): Notation`
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
import { updateStatus, Status } from '@mono-labs/tracker'
|
|
654
|
+
|
|
655
|
+
const resolved = updateStatus(notation, Status.RESOLVED)
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
#### `addTag(notation: Notation, tag: string): Notation`
|
|
659
|
+
|
|
660
|
+
Adds a tag if not already present. Returns the same object if the tag exists.
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
import { addTag } from '@mono-labs/tracker'
|
|
664
|
+
|
|
665
|
+
const tagged = addTag(notation, 'urgent')
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
#### `removeTag(notation: Notation, tag: string): Notation`
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
import { removeTag } from '@mono-labs/tracker'
|
|
672
|
+
|
|
673
|
+
const untagged = removeTag(notation, 'stale')
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
#### `setAssignee(notation: Notation, assignee: string): Notation`
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
import { setAssignee } from '@mono-labs/tracker'
|
|
680
|
+
|
|
681
|
+
const assigned = setAssignee(notation, 'Alice')
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Executor Framework
|
|
685
|
+
|
|
686
|
+
The executor provides a plugin-based system for dispatching parsed actions to handler functions.
|
|
687
|
+
|
|
688
|
+
#### `registerActionHandler(verb: string, handler: ActionHandler): void`
|
|
689
|
+
|
|
690
|
+
Registers a handler function for a specific action verb.
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
import { registerActionHandler } from '@mono-labs/tracker'
|
|
694
|
+
import type { ActionHandler } from '@mono-labs/tracker'
|
|
695
|
+
|
|
696
|
+
const myHandler: ActionHandler = async (action) => {
|
|
697
|
+
// Implement your logic here
|
|
698
|
+
return { success: true, message: 'Done', verb: action.verb }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
registerActionHandler('replace', myHandler)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
#### `executeAction(action: NotationAction): Promise<ActionResult>`
|
|
705
|
+
|
|
706
|
+
Dispatches an action to the registered handler for its verb. Returns `{ success: false }` if no handler is registered.
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
import { executeAction } from '@mono-labs/tracker'
|
|
710
|
+
|
|
711
|
+
for (const action of notation.actions) {
|
|
712
|
+
const result = await executeAction(action)
|
|
713
|
+
if (!result.success) {
|
|
714
|
+
console.error(`Failed: ${result.message}`)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
#### Built-in Stubs
|
|
720
|
+
|
|
721
|
+
Three stub handlers are exported for reference and testing. They return `success: true` with a descriptive message but perform no actual file operations:
|
|
722
|
+
|
|
723
|
+
- `handleReplace` — Stub for `replace` actions
|
|
724
|
+
- `handleRemove` — Stub for `remove` actions
|
|
725
|
+
- `handleRename` — Stub for `rename` actions
|
|
726
|
+
|
|
727
|
+
Register them if you want safe no-op handling:
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
import { registerActionHandler, handleReplace, handleRemove, handleRename } from '@mono-labs/tracker'
|
|
731
|
+
|
|
732
|
+
registerActionHandler('replace', handleReplace)
|
|
733
|
+
registerActionHandler('remove', handleRemove)
|
|
734
|
+
registerActionHandler('rename', handleRename)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
#### Writing a Custom Handler
|
|
738
|
+
|
|
739
|
+
```ts
|
|
740
|
+
import { registerActionHandler } from '@mono-labs/tracker'
|
|
741
|
+
import type { ActionHandler, NotationAction, ActionResult } from '@mono-labs/tracker'
|
|
742
|
+
|
|
743
|
+
const handleExtract: ActionHandler = async (action: NotationAction): Promise<ActionResult> => {
|
|
744
|
+
if (action.args.verb !== 'extract') {
|
|
745
|
+
return { success: false, message: 'Wrong verb', verb: action.verb }
|
|
746
|
+
}
|
|
747
|
+
const { target, destination } = action.args
|
|
748
|
+
// ... perform extraction logic ...
|
|
749
|
+
return { success: true, message: `Extracted ${target} to ${destination}`, verb: 'extract' }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
registerActionHandler('extract', handleExtract)
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## Full Type Reference
|
|
758
|
+
|
|
759
|
+
### Enums
|
|
760
|
+
|
|
761
|
+
All enums are defined as `const` objects with matching type aliases, enabling both runtime access and type safety.
|
|
762
|
+
|
|
763
|
+
#### `MarkerType`
|
|
764
|
+
|
|
765
|
+
```ts
|
|
766
|
+
'TODO' | 'FIXME' | 'BUG' | 'HACK' | 'NOTE' | 'OPTIMIZE' | 'SECURITY'
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
#### `Priority`
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
'minimal' | 'low' | 'medium' | 'high' | 'critical'
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
#### `RiskLevel`
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
'minimal' | 'low' | 'moderate' | 'severe' | 'critical'
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
#### `Status`
|
|
782
|
+
|
|
783
|
+
```ts
|
|
784
|
+
'open' | 'in_progress' | 'blocked' | 'resolved'
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### `CompoundingRate`
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
'low' | 'medium' | 'high'
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
#### `ActionVerb`
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
'replace' | 'remove' | 'rename' | 'insert' | 'extract' | 'move' | 'wrapIn' | 'generic'
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Interfaces
|
|
800
|
+
|
|
801
|
+
#### `Notation`
|
|
802
|
+
|
|
803
|
+
| Field | Type | Required | Description |
|
|
804
|
+
|---------------|---------------------|----------|--------------------------------------------|
|
|
805
|
+
| `id` | `string` | yes | Unique identifier (inline or generated) |
|
|
806
|
+
| `type` | `MarkerType` | yes | The marker type |
|
|
807
|
+
| `description` | `string` | yes | First-line description text |
|
|
808
|
+
| `body` | `string[]` | yes | Continuation comment lines |
|
|
809
|
+
| `codeContext` | `string[]` | yes | Code lines following the notation block |
|
|
810
|
+
| `location` | `SourceLocation` | yes | File, line, column, and optional endLine |
|
|
811
|
+
| `author` | `string` | no | Author name from attributes |
|
|
812
|
+
| `assignee` | `string` | no | Assignee name |
|
|
813
|
+
| `priority` | `Priority` | no | Priority level |
|
|
814
|
+
| `risk` | `RiskLevel` | no | Risk level |
|
|
815
|
+
| `status` | `Status` | yes | Current status (default: `'open'`) |
|
|
816
|
+
| `tags` | `string[]` | yes | Tags parsed from attributes |
|
|
817
|
+
| `dueDate` | `string` | no | ISO date string |
|
|
818
|
+
| `createdDate` | `string` | no | ISO date string |
|
|
819
|
+
| `performance` | `PerformanceImpact` | no | Performance before/after measurement |
|
|
820
|
+
| `debt` | `TechnicalDebt` | no | Estimated debt hours and compounding rate |
|
|
821
|
+
| `actions` | `NotationAction[]` | yes | Parsed action instructions |
|
|
822
|
+
| `relationships` | `string[]` | yes | IDs of related notations |
|
|
823
|
+
| `rawBlock` | `string` | yes | Original raw text of the notation block |
|
|
824
|
+
| `scannedAt` | `string` | yes | ISO timestamp of when the notation was scanned |
|
|
825
|
+
|
|
826
|
+
#### `SourceLocation`
|
|
827
|
+
|
|
828
|
+
| Field | Type | Required | Description |
|
|
829
|
+
|-----------|----------|----------|--------------------------------|
|
|
830
|
+
| `file` | `string` | yes | File path |
|
|
831
|
+
| `line` | `number` | yes | Start line (1-indexed) |
|
|
832
|
+
| `column` | `number` | yes | Column offset (1-indexed) |
|
|
833
|
+
| `endLine` | `number` | no | End line of the notation block |
|
|
834
|
+
|
|
835
|
+
#### `PerformanceImpact`
|
|
836
|
+
|
|
837
|
+
| Field | Type | Description |
|
|
838
|
+
|----------|----------|--------------------------------------|
|
|
839
|
+
| `before` | `string` | Value before (e.g., `'2000ms'`) |
|
|
840
|
+
| `after` | `string` | Value after (e.g., `'100ms'`) |
|
|
841
|
+
| `unit` | `string` | Unit from the "after" value (`ms`, `s`, `us`) |
|
|
842
|
+
|
|
843
|
+
#### `TechnicalDebt`
|
|
844
|
+
|
|
845
|
+
| Field | Type | Description |
|
|
846
|
+
|---------------|-------------------|--------------------------------|
|
|
847
|
+
| `hours` | `number` | Estimated debt in hours |
|
|
848
|
+
| `compounding` | `CompoundingRate` | How fast the debt grows |
|
|
849
|
+
|
|
850
|
+
#### `NotationQuery`
|
|
851
|
+
|
|
852
|
+
| Field | Type | Description |
|
|
853
|
+
|-------------|--------------------------------|--------------------------------------|
|
|
854
|
+
| `type` | `MarkerType \| MarkerType[]` | Filter by marker type(s) |
|
|
855
|
+
| `tags` | `string[]` | Filter by tag (OR match) |
|
|
856
|
+
| `priority` | `Priority \| Priority[]` | Filter by priority level(s) |
|
|
857
|
+
| `status` | `Status \| Status[]` | Filter by status(es) |
|
|
858
|
+
| `file` | `string` | Substring match on file path |
|
|
859
|
+
| `assignee` | `string` | Exact assignee match |
|
|
860
|
+
| `overdue` | `boolean` | Only overdue, non-resolved notations |
|
|
861
|
+
| `blocked` | `boolean` | Only blocked notations |
|
|
862
|
+
| `search` | `string` | Full-text search (description, body, tags) |
|
|
863
|
+
| `dueBefore` | `string` | Due on or before this ISO date |
|
|
864
|
+
| `dueAfter` | `string` | Due on or after this ISO date |
|
|
865
|
+
|
|
866
|
+
#### `NotationStats`
|
|
867
|
+
|
|
868
|
+
| Field | Type | Description |
|
|
869
|
+
|------------------|-------------------------|----------------------------------|
|
|
870
|
+
| `total` | `number` | Total notation count |
|
|
871
|
+
| `byType` | `Record<string, number>`| Count per marker type |
|
|
872
|
+
| `byPriority` | `Record<string, number>`| Count per priority level |
|
|
873
|
+
| `byStatus` | `Record<string, number>`| Count per status |
|
|
874
|
+
| `byTag` | `Record<string, number>`| Count per tag |
|
|
875
|
+
| `byAssignee` | `Record<string, number>`| Count per assignee |
|
|
876
|
+
| `overdue` | `number` | Number of overdue notations |
|
|
877
|
+
| `blocked` | `number` | Number of blocked notations |
|
|
878
|
+
| `totalDebtHours` | `number` | Sum of all debt hours |
|
|
879
|
+
|
|
880
|
+
#### `NotationAction`
|
|
881
|
+
|
|
882
|
+
| Field | Type | Description |
|
|
883
|
+
|--------|--------------|--------------------------------|
|
|
884
|
+
| `verb` | `ActionVerb` | The action verb |
|
|
885
|
+
| `raw` | `string` | Original raw action string |
|
|
886
|
+
| `args` | `ActionArgs` | Parsed arguments (discriminated union) |
|
|
887
|
+
|
|
888
|
+
#### `ActionArgs` Variants
|
|
889
|
+
|
|
890
|
+
| Variant | Fields |
|
|
891
|
+
|---------------|-------------------------------------------------|
|
|
892
|
+
| `ReplaceArgs` | `verb: 'replace'`, `target`, `replacement` |
|
|
893
|
+
| `RemoveArgs` | `verb: 'remove'`, `target` |
|
|
894
|
+
| `RenameArgs` | `verb: 'rename'`, `from`, `to` |
|
|
895
|
+
| `InsertArgs` | `verb: 'insert'`, `content`, `position` (`'before' \| 'after'`), `anchor` |
|
|
896
|
+
| `ExtractArgs` | `verb: 'extract'`, `target`, `destination` |
|
|
897
|
+
| `MoveArgs` | `verb: 'move'`, `target`, `destination` |
|
|
898
|
+
| `WrapInArgs` | `verb: 'wrapIn'`, `target`, `wrapper` |
|
|
899
|
+
| `GenericArgs` | `verb: 'generic'`, `description` |
|
|
900
|
+
|
|
901
|
+
#### `TrackerConfig`
|
|
902
|
+
|
|
903
|
+
| Field | Type | Description |
|
|
904
|
+
|---------------|----------------|-----------------------------------|
|
|
905
|
+
| `rootDir` | `string` | Project root directory |
|
|
906
|
+
| `include` | `string[]` | Glob patterns to include |
|
|
907
|
+
| `exclude` | `string[]` | Glob patterns to exclude |
|
|
908
|
+
| `markers` | `MarkerType[]` | Marker types to recognize |
|
|
909
|
+
| `storagePath` | `string` | JSONL file path |
|
|
910
|
+
| `idPrefix` | `string` | Prefix for generated IDs |
|
|
911
|
+
|
|
912
|
+
#### `ValidationError`
|
|
913
|
+
|
|
914
|
+
| Field | Type | Description |
|
|
915
|
+
|--------------|----------|-------------------------------------|
|
|
916
|
+
| `notationId` | `string` | ID of the notation with the error |
|
|
917
|
+
| `field` | `string` | Field name that failed validation |
|
|
918
|
+
| `message` | `string` | Human-readable error message |
|
|
919
|
+
|
|
920
|
+
#### `ActionResult`
|
|
921
|
+
|
|
922
|
+
| Field | Type | Description |
|
|
923
|
+
|-----------|-----------|----------------------------------|
|
|
924
|
+
| `success` | `boolean` | Whether the action succeeded |
|
|
925
|
+
| `message` | `string` | Result or error message |
|
|
926
|
+
| `verb` | `string` | The action verb that was executed|
|
|
927
|
+
|
|
928
|
+
#### `ActionHandler`
|
|
929
|
+
|
|
930
|
+
```ts
|
|
931
|
+
type ActionHandler = (action: NotationAction) => Promise<ActionResult>
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
#### `ParsedAttributes`
|
|
935
|
+
|
|
936
|
+
| Field | Type | Description |
|
|
937
|
+
|----------------|---------------------|--------------------------------|
|
|
938
|
+
| `author` | `string \| undefined` | Parsed author |
|
|
939
|
+
| `assignee` | `string \| undefined` | Parsed assignee |
|
|
940
|
+
| `priority` | `Priority \| undefined` | Parsed priority |
|
|
941
|
+
| `risk` | `RiskLevel \| undefined` | Parsed risk level |
|
|
942
|
+
| `tags` | `string[]` | Parsed tags |
|
|
943
|
+
| `dueDate` | `string \| undefined` | Parsed due date (ISO) |
|
|
944
|
+
| `createdDate` | `string \| undefined` | Parsed created date (ISO) |
|
|
945
|
+
| `performance` | `PerformanceImpact \| undefined` | Parsed performance |
|
|
946
|
+
| `debt` | `TechnicalDebt \| undefined` | Parsed debt |
|
|
947
|
+
| `relationships`| `string[]` | Parsed relationship IDs |
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## Architecture
|
|
952
|
+
|
|
953
|
+
### Layer Diagram
|
|
954
|
+
|
|
955
|
+
```
|
|
956
|
+
┌─────────────────────────────────────────────────────────┐
|
|
957
|
+
│ Executor │
|
|
958
|
+
│ registerActionHandler · executeAction │
|
|
959
|
+
├─────────────────────────────────────────────────────────┤
|
|
960
|
+
│ Manager │
|
|
961
|
+
│ NotationManager · query · validate · stats · updaters │
|
|
962
|
+
├─────────────────────────────────────────────────────────┤
|
|
963
|
+
│ Scanner │
|
|
964
|
+
│ scanFiles · parseFileContent · parseAttributes │
|
|
965
|
+
├─────────────────────────────────────────────────────────┤
|
|
966
|
+
│ Storage │
|
|
967
|
+
│ JsonlStorage · loadConfig │
|
|
968
|
+
├─────────────────────────────────────────────────────────┤
|
|
969
|
+
│ Utils │
|
|
970
|
+
│ generateId · generateStableId · parseDate │
|
|
971
|
+
├─────────────────────────────────────────────────────────┤
|
|
972
|
+
│ Types │
|
|
973
|
+
│ enums · Notation · Action · Config · Query · Stats │
|
|
974
|
+
└─────────────────────────────────────────────────────────┘
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
Each layer depends only on layers below it.
|
|
978
|
+
|
|
979
|
+
### Source Tree
|
|
980
|
+
|
|
981
|
+
```
|
|
982
|
+
packages/tracker/
|
|
983
|
+
├── src/
|
|
984
|
+
│ ├── index.ts # Public barrel export
|
|
985
|
+
│ ├── types/
|
|
986
|
+
│ │ ├── index.ts # Type barrel
|
|
987
|
+
│ │ ├── enums.ts # MarkerType, Priority, RiskLevel, Status, CompoundingRate
|
|
988
|
+
│ │ ├── notation.ts # Notation, SourceLocation, NotationQuery, NotationStats
|
|
989
|
+
│ │ ├── action.ts # ActionVerb, NotationAction, all ActionArgs variants
|
|
990
|
+
│ │ └── config.ts # TrackerConfig, DEFAULT_CONFIG
|
|
991
|
+
│ ├── utils/
|
|
992
|
+
│ │ ├── index.ts # Utils barrel
|
|
993
|
+
│ │ ├── id-generator.ts # generateId, generateStableId
|
|
994
|
+
│ │ ├── id-generator.test.ts
|
|
995
|
+
│ │ ├── date-parser.ts # parseDate, isOverdue
|
|
996
|
+
│ │ └── date-parser.test.ts
|
|
997
|
+
│ ├── storage/
|
|
998
|
+
│ │ ├── index.ts # Storage barrel
|
|
999
|
+
│ │ ├── jsonl-storage.ts # JsonlStorage class
|
|
1000
|
+
│ │ ├── jsonl-storage.test.ts
|
|
1001
|
+
│ │ └── config-loader.ts # loadConfig
|
|
1002
|
+
│ ├── scanner/
|
|
1003
|
+
│ │ ├── index.ts # Scanner barrel
|
|
1004
|
+
│ │ ├── file-scanner.ts # scanFiles (glob + read + parse)
|
|
1005
|
+
│ │ ├── notation-parser.ts # parseFileContent
|
|
1006
|
+
│ │ ├── notation-parser.test.ts
|
|
1007
|
+
│ │ ├── attribute-parser.ts # parseAttributes (3 style strategies)
|
|
1008
|
+
│ │ ├── attribute-parser.test.ts
|
|
1009
|
+
│ │ ├── action-parser.ts # parseActions (chained call parser)
|
|
1010
|
+
│ │ └── action-parser.test.ts
|
|
1011
|
+
│ ├── manager/
|
|
1012
|
+
│ │ ├── index.ts # Manager barrel
|
|
1013
|
+
│ │ ├── notation-manager.ts # NotationManager class
|
|
1014
|
+
│ │ ├── notation-manager.test.ts
|
|
1015
|
+
│ │ ├── notation-updater.ts # updateStatus, addTag, removeTag, setAssignee
|
|
1016
|
+
│ │ ├── relationship-manager.ts # getBlockers, isBlocked, detectCircularDependencies
|
|
1017
|
+
│ │ ├── validator.ts # validateNotation, validateAll
|
|
1018
|
+
│ │ └── stats.ts # computeStats
|
|
1019
|
+
│ └── executor/
|
|
1020
|
+
│ ├── index.ts # Executor barrel
|
|
1021
|
+
│ ├── action-executor.ts # registerActionHandler, executeAction
|
|
1022
|
+
│ └── actions/
|
|
1023
|
+
│ ├── index.ts # Action stubs barrel
|
|
1024
|
+
│ ├── replace-action.ts # handleReplace stub
|
|
1025
|
+
│ ├── remove-action.ts # handleRemove stub
|
|
1026
|
+
│ └── rename-action.ts # handleRename stub
|
|
1027
|
+
├── package.json
|
|
1028
|
+
├── tsconfig.json
|
|
1029
|
+
└── vitest.config.ts
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Design Decisions
|
|
1033
|
+
|
|
1034
|
+
- **JSONL storage** — Append-friendly format; each line is an independent JSON object, making it resilient to partial writes and easy to stream
|
|
1035
|
+
- **Atomic writes** — `writeAll` writes to a `.tmp` file then renames, preventing data corruption on crash
|
|
1036
|
+
- **Stable IDs** — `generateStableId` uses SHA-256 of `file:line`, so re-scanning produces the same IDs for unchanged notations
|
|
1037
|
+
- **Corrupt line resilience** — `readAll` silently skips unparseable lines, so a single corrupt entry doesn't break the entire store
|
|
1038
|
+
- **Immutable updaters** — Mutation helpers (`updateStatus`, `addTag`, etc.) return new objects, leaving originals unchanged
|
|
1039
|
+
- **Plugin executor** — The handler registry decouples action parsing from execution, allowing consumers to implement their own file-modification logic
|
|
1040
|
+
|
|
1041
|
+
---
|
|
1042
|
+
|
|
1043
|
+
## Contributor Guide
|
|
1044
|
+
|
|
1045
|
+
### Prerequisites
|
|
1046
|
+
|
|
1047
|
+
- **Node.js** 20+
|
|
1048
|
+
- **Yarn** 1.x (classic)
|
|
1049
|
+
- **TypeScript** 5.9+
|
|
1050
|
+
|
|
1051
|
+
### Setup
|
|
1052
|
+
|
|
1053
|
+
```bash
|
|
1054
|
+
git clone <repo-url>
|
|
1055
|
+
cd mono-labs-cli
|
|
1056
|
+
yarn install
|
|
1057
|
+
yarn workspace @mono-labs/tracker build
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
### Project Structure
|
|
1061
|
+
|
|
1062
|
+
| Directory | Purpose |
|
|
1063
|
+
|----------------|--------------------------------------------------------|
|
|
1064
|
+
| `src/types/` | All TypeScript types, enums, and config defaults |
|
|
1065
|
+
| `src/utils/` | Pure utility functions (ID generation, date parsing) |
|
|
1066
|
+
| `src/storage/` | JSONL persistence and config file loading |
|
|
1067
|
+
| `src/scanner/` | File discovery, notation parsing, attribute/action extraction |
|
|
1068
|
+
| `src/manager/` | High-level facade, querying, validation, stats, mutation helpers |
|
|
1069
|
+
| `src/executor/`| Action dispatch registry and built-in handler stubs |
|
|
1070
|
+
|
|
1071
|
+
### Development Workflow
|
|
1072
|
+
|
|
1073
|
+
```bash
|
|
1074
|
+
# Edit source files in src/
|
|
1075
|
+
# Build the package
|
|
1076
|
+
yarn workspace @mono-labs/tracker build
|
|
1077
|
+
|
|
1078
|
+
# Run tests
|
|
1079
|
+
yarn workspace @mono-labs/tracker test
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
### Testing
|
|
1083
|
+
|
|
1084
|
+
Tests use [Vitest](https://vitest.dev/) with colocated `.test.ts` files. Run them with:
|
|
1085
|
+
|
|
1086
|
+
```bash
|
|
1087
|
+
yarn workspace @mono-labs/tracker test
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
#### Test Files
|
|
1091
|
+
|
|
1092
|
+
| File | Covers |
|
|
1093
|
+
|-------------------------------------|------------------------------------------------------------|
|
|
1094
|
+
| `utils/id-generator.test.ts` | `generateId`, `generateStableId` — format, uniqueness, determinism |
|
|
1095
|
+
| `utils/date-parser.test.ts` | `parseDate`, `isOverdue` — ISO, US, relative formats |
|
|
1096
|
+
| `scanner/notation-parser.test.ts` | `parseFileContent` — marker extraction, multi-line, code context, inline IDs |
|
|
1097
|
+
| `scanner/attribute-parser.test.ts` | `parseAttributes` — all 3 styles, priority/risk maps, relationships |
|
|
1098
|
+
| `scanner/action-parser.test.ts` | `parseActions` — all verbs, chained calls, edge cases |
|
|
1099
|
+
| `storage/jsonl-storage.test.ts` | `JsonlStorage` — read/write/append, atomic writes, corrupt line handling |
|
|
1100
|
+
| `manager/notation-manager.test.ts` | `NotationManager`, `computeStats`, relationships, updaters, validation |
|
|
1101
|
+
|
|
1102
|
+
#### Writing Tests
|
|
1103
|
+
|
|
1104
|
+
Tests follow these patterns:
|
|
1105
|
+
|
|
1106
|
+
```ts
|
|
1107
|
+
// Helper factory — override only what you need
|
|
1108
|
+
function makeNotation(overrides: Partial<Notation> = {}): Notation {
|
|
1109
|
+
return {
|
|
1110
|
+
id: 'N-test001',
|
|
1111
|
+
type: 'TODO',
|
|
1112
|
+
description: 'Test notation',
|
|
1113
|
+
body: [],
|
|
1114
|
+
codeContext: [],
|
|
1115
|
+
location: { file: 'test.ts', line: 1, column: 1 },
|
|
1116
|
+
status: 'open',
|
|
1117
|
+
tags: [],
|
|
1118
|
+
actions: [],
|
|
1119
|
+
relationships: [],
|
|
1120
|
+
rawBlock: '// TODO: Test',
|
|
1121
|
+
scannedAt: '2026-01-01T00:00:00.000Z',
|
|
1122
|
+
...overrides,
|
|
1123
|
+
} as Notation
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Temp directory pattern for storage tests
|
|
1127
|
+
let tmpDir: string
|
|
1128
|
+
beforeEach(() => {
|
|
1129
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tracker-test-'))
|
|
1130
|
+
})
|
|
1131
|
+
afterEach(() => {
|
|
1132
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
1133
|
+
})
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### Adding a New Marker Type
|
|
1137
|
+
|
|
1138
|
+
1. **`src/types/enums.ts`** — Add the new value to the `MarkerType` const object
|
|
1139
|
+
2. **`src/types/config.ts`** — Add it to `DEFAULT_CONFIG.markers`
|
|
1140
|
+
3. **`src/scanner/notation-parser.ts`** — Add it to the `MARKER_REGEX` alternation
|
|
1141
|
+
4. **Tests** — Add test cases in `notation-parser.test.ts`
|
|
1142
|
+
|
|
1143
|
+
### Adding a New Attribute
|
|
1144
|
+
|
|
1145
|
+
1. **`src/scanner/attribute-parser.ts`** — Add a case in `applyKeyValue()` for the new key
|
|
1146
|
+
2. **`src/scanner/attribute-parser.ts`** — Update `ParsedAttributes` interface if a new field is needed
|
|
1147
|
+
3. **`src/types/notation.ts`** — Add the field to `Notation` if it's a new top-level field
|
|
1148
|
+
4. **`src/scanner/notation-parser.ts`** — Map the parsed attribute to the `Notation` object
|
|
1149
|
+
5. **Tests** — Add test cases in `attribute-parser.test.ts`
|
|
1150
|
+
|
|
1151
|
+
### Adding a New Action Verb
|
|
1152
|
+
|
|
1153
|
+
1. **`src/types/action.ts`** — Add the verb to `ActionVerb` and create a new `*Args` interface; add it to the `ActionArgs` union
|
|
1154
|
+
2. **`src/scanner/action-parser.ts`** — Add a case in `buildActionArgs()` for the new verb
|
|
1155
|
+
3. **`src/executor/actions/`** — Create a stub handler file
|
|
1156
|
+
4. **`src/executor/actions/index.ts`** — Export the new stub
|
|
1157
|
+
5. **`src/executor/index.ts`** — Re-export the new stub
|
|
1158
|
+
6. **`src/index.ts`** — Export the new type and handler
|
|
1159
|
+
7. **Tests** — Add test cases in `action-parser.test.ts`
|
|
1160
|
+
|
|
1161
|
+
### Adding a New Query Filter
|
|
1162
|
+
|
|
1163
|
+
1. **`src/types/notation.ts`** — Add the field to `NotationQuery`
|
|
1164
|
+
2. **`src/manager/notation-manager.ts`** — Add the filter logic in the `query()` method's filter chain
|
|
1165
|
+
3. **Tests** — Add test cases in `notation-manager.test.ts`
|
|
1166
|
+
|
|
1167
|
+
### Code Style
|
|
1168
|
+
|
|
1169
|
+
- **Tabs** for indentation
|
|
1170
|
+
- **Single quotes** for strings
|
|
1171
|
+
- **Trailing commas** in multi-line constructs
|
|
1172
|
+
- **No semicolons**
|
|
1173
|
+
- Match the existing style — when in doubt, look at surrounding code
|
|
1174
|
+
|
|
1175
|
+
### Monorepo Integration
|
|
1176
|
+
|
|
1177
|
+
The tracker package lives within the `mono-labs-cli` monorepo:
|
|
1178
|
+
|
|
1179
|
+
- **`scripts/bump-version.js`** — Bumps version across all packages (root, shared, project, expo, cli, dev, tracker) in lockstep. Usage: `node scripts/bump-version.js [patch|minor|major]`
|
|
1180
|
+
- **Deploy** — `yarn workspace @mono-labs/tracker deploy` builds and publishes to npm
|
|
1181
|
+
- **Release scripts** — `release:patch`, `release:minor`, `release:major` handle version bump + publish in one command
|
|
1182
|
+
|
|
1183
|
+
### PR Checklist
|
|
1184
|
+
|
|
1185
|
+
- [ ] `yarn workspace @mono-labs/tracker build` passes with no errors
|
|
1186
|
+
- [ ] `yarn workspace @mono-labs/tracker test` passes (all 102+ tests green)
|
|
1187
|
+
- [ ] New types are exported from barrel files (`src/*/index.ts` and `src/index.ts`)
|
|
1188
|
+
- [ ] No regressions in existing tests
|
|
1189
|
+
- [ ] Code follows existing style (tabs, single quotes, no semicolons, trailing commas)
|
|
1190
|
+
- [ ] Tests added for new functionality
|
|
1191
|
+
|
|
1192
|
+
---
|
|
1193
|
+
|
|
1194
|
+
## License
|
|
1195
|
+
|
|
1196
|
+
MIT
|