@mono-labs/tracker 0.1.269 → 0.1.278

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 (113) hide show
  1. package/dist/dashboard/cli.js +42 -0
  2. package/dist/dashboard/server.d.ts.map +1 -1
  3. package/dist/dashboard/server.js +466 -3
  4. package/dist/dashboard/watcher.d.ts +1 -1
  5. package/dist/dashboard/watcher.d.ts.map +1 -1
  6. package/dist/dashboard/watcher.js +1 -1
  7. package/dist/executor/action-executor.d.ts +3 -3
  8. package/dist/executor/action-executor.d.ts.map +1 -1
  9. package/dist/executor/action-executor.js +2 -2
  10. package/dist/executor/actions/extract-action.d.ts +4 -0
  11. package/dist/executor/actions/extract-action.d.ts.map +1 -0
  12. package/dist/executor/actions/extract-action.js +85 -0
  13. package/dist/executor/actions/index.d.ts +4 -0
  14. package/dist/executor/actions/index.d.ts.map +1 -1
  15. package/dist/executor/actions/index.js +9 -1
  16. package/dist/executor/actions/insert-action.d.ts +4 -0
  17. package/dist/executor/actions/insert-action.d.ts.map +1 -0
  18. package/dist/executor/actions/insert-action.js +74 -0
  19. package/dist/executor/actions/move-action.d.ts +4 -0
  20. package/dist/executor/actions/move-action.d.ts.map +1 -0
  21. package/dist/executor/actions/move-action.js +83 -0
  22. package/dist/executor/actions/remove-action.d.ts +2 -2
  23. package/dist/executor/actions/remove-action.d.ts.map +1 -1
  24. package/dist/executor/actions/remove-action.js +70 -6
  25. package/dist/executor/actions/rename-action.d.ts +2 -2
  26. package/dist/executor/actions/rename-action.d.ts.map +1 -1
  27. package/dist/executor/actions/rename-action.js +69 -6
  28. package/dist/executor/actions/replace-action.d.ts +2 -2
  29. package/dist/executor/actions/replace-action.d.ts.map +1 -1
  30. package/dist/executor/actions/replace-action.js +65 -6
  31. package/dist/executor/actions/wrap-action.d.ts +4 -0
  32. package/dist/executor/actions/wrap-action.d.ts.map +1 -0
  33. package/dist/executor/actions/wrap-action.js +97 -0
  34. package/dist/executor/index.d.ts +1 -1
  35. package/dist/executor/index.d.ts.map +1 -1
  36. package/dist/executor/index.js +21 -1
  37. package/dist/governance/deprecation-tracker.d.ts +21 -0
  38. package/dist/governance/deprecation-tracker.d.ts.map +1 -0
  39. package/dist/governance/deprecation-tracker.js +55 -0
  40. package/dist/governance/index.d.ts +5 -0
  41. package/dist/governance/index.d.ts.map +1 -0
  42. package/dist/governance/index.js +7 -0
  43. package/dist/governance/security-gate.d.ts +15 -0
  44. package/dist/governance/security-gate.d.ts.map +1 -0
  45. package/dist/governance/security-gate.js +40 -0
  46. package/dist/governance/security-gate.test.d.ts +2 -0
  47. package/dist/governance/security-gate.test.d.ts.map +1 -0
  48. package/dist/governance/security-gate.test.js +67 -0
  49. package/dist/index.d.ts +13 -6
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +20 -1
  52. package/dist/integrations/ai-suggest.d.ts +8 -0
  53. package/dist/integrations/ai-suggest.d.ts.map +1 -0
  54. package/dist/integrations/ai-suggest.js +106 -0
  55. package/dist/integrations/github-issues.d.ts +8 -0
  56. package/dist/integrations/github-issues.d.ts.map +1 -0
  57. package/dist/integrations/github-issues.js +84 -0
  58. package/dist/integrations/index.d.ts +5 -0
  59. package/dist/integrations/index.d.ts.map +1 -0
  60. package/dist/integrations/index.js +9 -0
  61. package/dist/integrations/jira-issues.d.ts +8 -0
  62. package/dist/integrations/jira-issues.d.ts.map +1 -0
  63. package/dist/integrations/jira-issues.js +93 -0
  64. package/dist/manager/health-score.d.ts +3 -0
  65. package/dist/manager/health-score.d.ts.map +1 -0
  66. package/dist/manager/health-score.js +32 -0
  67. package/dist/manager/index.d.ts +3 -0
  68. package/dist/manager/index.d.ts.map +1 -1
  69. package/dist/manager/index.js +5 -1
  70. package/dist/manager/notation-manager.test.js +5 -1
  71. package/dist/manager/projection.d.ts +15 -0
  72. package/dist/manager/projection.d.ts.map +1 -0
  73. package/dist/manager/projection.js +56 -0
  74. package/dist/scanner/action-serializer.d.ts +4 -0
  75. package/dist/scanner/action-serializer.d.ts.map +1 -0
  76. package/dist/scanner/action-serializer.js +54 -0
  77. package/dist/scanner/attribute-parser.d.ts +2 -0
  78. package/dist/scanner/attribute-parser.d.ts.map +1 -1
  79. package/dist/scanner/attribute-parser.js +11 -0
  80. package/dist/scanner/git-blame.d.ts +3 -0
  81. package/dist/scanner/git-blame.d.ts.map +1 -0
  82. package/dist/scanner/git-blame.js +81 -0
  83. package/dist/scanner/index.d.ts +1 -0
  84. package/dist/scanner/index.d.ts.map +1 -1
  85. package/dist/scanner/index.js +4 -1
  86. package/dist/scanner/notation-parser.d.ts.map +1 -1
  87. package/dist/scanner/notation-parser.js +3 -1
  88. package/dist/storage/config-loader.d.ts +6 -0
  89. package/dist/storage/config-loader.d.ts.map +1 -1
  90. package/dist/storage/config-loader.js +10 -0
  91. package/dist/storage/index.d.ts +4 -1
  92. package/dist/storage/index.d.ts.map +1 -1
  93. package/dist/storage/index.js +4 -1
  94. package/dist/storage/snapshot-storage.d.ts +16 -0
  95. package/dist/storage/snapshot-storage.d.ts.map +1 -0
  96. package/dist/storage/snapshot-storage.js +106 -0
  97. package/dist/storage/snapshot-storage.test.d.ts +2 -0
  98. package/dist/storage/snapshot-storage.test.d.ts.map +1 -0
  99. package/dist/storage/snapshot-storage.test.js +117 -0
  100. package/dist/types/config.d.ts +22 -0
  101. package/dist/types/config.d.ts.map +1 -1
  102. package/dist/types/config.js +5 -1
  103. package/dist/types/enums.d.ts +1 -0
  104. package/dist/types/enums.d.ts.map +1 -1
  105. package/dist/types/enums.js +1 -0
  106. package/dist/types/index.d.ts +1 -1
  107. package/dist/types/index.d.ts.map +1 -1
  108. package/dist/types/notation.d.ts +9 -0
  109. package/dist/types/notation.d.ts.map +1 -1
  110. package/dist-dashboard/assets/index-CzsCRhkp.js +197 -0
  111. package/dist-dashboard/assets/index-DUqAN9SG.css +1 -0
  112. package/dist-dashboard/index.html +23 -0
  113. package/package.json +33 -8
@@ -3,8 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const shared_1 = require("@mono-labs/shared");
4
4
  const storage_1 = require("../storage");
5
5
  const server_1 = require("./server");
6
+ const scanner_1 = require("../scanner");
7
+ const security_gate_1 = require("../governance/security-gate");
6
8
  async function main() {
7
9
  const args = process.argv.slice(2);
10
+ // Check for subcommand
11
+ if (args[0] === 'gate') {
12
+ return runGate(args.slice(1));
13
+ }
8
14
  let port = 4321;
9
15
  let rootOverride;
10
16
  for (let i = 0; i < args.length; i++) {
@@ -32,6 +38,42 @@ async function main() {
32
38
  process.on('SIGINT', shutdown);
33
39
  process.on('SIGTERM', shutdown);
34
40
  }
41
+ async function runGate(args) {
42
+ let rootOverride;
43
+ let format = 'text';
44
+ for (let i = 0; i < args.length; i++) {
45
+ if (args[i] === '--root' && args[i + 1]) {
46
+ rootOverride = args[i + 1];
47
+ i++;
48
+ }
49
+ else if (args[i] === '--format' && args[i + 1]) {
50
+ format = args[i + 1];
51
+ i++;
52
+ }
53
+ }
54
+ const { root: detectedRoot } = (0, shared_1.findWorkspaceRoot)(rootOverride);
55
+ const projectRoot = rootOverride ?? detectedRoot;
56
+ const config = (0, storage_1.loadConfig)(projectRoot);
57
+ const notations = await (0, scanner_1.scanFiles)(config, projectRoot);
58
+ const result = (0, security_gate_1.evaluateSecurityGate)(notations, config);
59
+ if (format === 'json') {
60
+ console.log(JSON.stringify(result, null, 2));
61
+ }
62
+ else {
63
+ if (result.passed) {
64
+ console.log(`[tracker] Security gate: PASSED`);
65
+ console.log(` ${result.summary}`);
66
+ }
67
+ else {
68
+ console.log(`[tracker] Security gate: FAILED`);
69
+ console.log(` ${result.summary}`);
70
+ for (const v of result.violations) {
71
+ console.log(` - [${v.priority}] ${v.notationId}: ${v.description}`);
72
+ }
73
+ }
74
+ }
75
+ process.exit(result.passed ? 0 : 1);
76
+ }
35
77
  main().catch((err) => {
36
78
  console.error('[tracker] Failed to start:', err);
37
79
  process.exit(1);
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAM/D,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAkIpF"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAkB/D,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CA8dpF"}
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startDashboard = startDashboard;
7
+ const fs_1 = __importDefault(require("fs"));
7
8
  const http_1 = __importDefault(require("http"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const express_1 = __importDefault(require("express"));
@@ -11,16 +12,47 @@ const cors_1 = __importDefault(require("cors"));
11
12
  const ws_1 = require("ws");
12
13
  const manager_1 = require("../manager");
13
14
  const scanner_1 = require("../scanner");
15
+ const action_serializer_1 = require("../scanner/action-serializer");
14
16
  const watcher_1 = require("./watcher");
17
+ const snapshot_storage_1 = require("../storage/snapshot-storage");
18
+ const config_loader_1 = require("../storage/config-loader");
19
+ const health_score_1 = require("../manager/health-score");
20
+ const security_gate_1 = require("../governance/security-gate");
21
+ const deprecation_tracker_1 = require("../governance/deprecation-tracker");
22
+ const projection_1 = require("../manager/projection");
23
+ const git_blame_1 = require("../scanner/git-blame");
24
+ const executor_1 = require("../executor");
25
+ const github_issues_1 = require("../integrations/github-issues");
26
+ const jira_issues_1 = require("../integrations/jira-issues");
27
+ const ai_suggest_1 = require("../integrations/ai-suggest");
15
28
  async function startDashboard(opts) {
16
29
  const { projectRoot, config, port = 4321 } = opts;
30
+ const secrets = (0, config_loader_1.loadSecrets)();
17
31
  const manager = new manager_1.NotationManager(config);
32
+ // Snapshot storage
33
+ const snapshotPath = path_1.default.isAbsolute(config.snapshotPath)
34
+ ? config.snapshotPath
35
+ : path_1.default.join(projectRoot, config.snapshotPath);
36
+ const snapshotStorage = new snapshot_storage_1.SnapshotStorage(snapshotPath);
18
37
  // Initial scan
19
38
  const notations = await (0, scanner_1.scanFiles)(config, projectRoot);
20
39
  manager.setAll(notations);
40
+ // Take initial snapshot
41
+ await takeSnapshotIfNewDay(manager, snapshotStorage);
42
+ // Optionally run git blame
43
+ if (config.gitBlame) {
44
+ runBlameAsync(projectRoot, manager, () => broadcastUpdate());
45
+ }
21
46
  const app = (0, express_1.default)();
22
47
  app.use((0, cors_1.default)());
23
48
  app.use(express_1.default.json());
49
+ // --- Helper to compute current gate result ---
50
+ function currentGateResult() {
51
+ return (0, security_gate_1.evaluateSecurityGate)(manager.getAll(), config);
52
+ }
53
+ function currentHealthScore() {
54
+ return (0, health_score_1.computeHealthScore)(manager.getAll());
55
+ }
24
56
  // --- API Routes ---
25
57
  app.get('/api/notations', (req, res) => {
26
58
  const query = {};
@@ -60,12 +92,26 @@ async function startDashboard(opts) {
60
92
  res.json(manager.stats());
61
93
  });
62
94
  app.get('/api/config', (_req, res) => {
63
- res.json({ config, projectRoot });
95
+ // Strip secrets — never expose tokens
96
+ const safeConfig = { ...config };
97
+ res.json({
98
+ config: safeConfig,
99
+ projectRoot,
100
+ integrations: {
101
+ github: !!config.integrations.github && !!secrets.githubToken,
102
+ jira: !!config.integrations.jira && !!secrets.jiraToken,
103
+ ai: !!secrets.aiKey,
104
+ },
105
+ });
64
106
  });
65
107
  app.post('/api/scan', async (_req, res) => {
66
108
  try {
67
109
  const fresh = await (0, scanner_1.scanFiles)(config, projectRoot);
68
110
  manager.setAll(fresh);
111
+ await takeSnapshotIfNewDay(manager, snapshotStorage);
112
+ if (config.gitBlame) {
113
+ runBlameAsync(projectRoot, manager, () => broadcastUpdate());
114
+ }
69
115
  broadcastUpdate();
70
116
  res.json({ count: fresh.length });
71
117
  }
@@ -73,6 +119,284 @@ async function startDashboard(opts) {
73
119
  res.status(500).json({ error: 'Scan failed' });
74
120
  }
75
121
  });
122
+ app.get('/api/notations/:id/source', (req, res) => {
123
+ const notation = manager.getById(req.params.id);
124
+ if (!notation) {
125
+ res.status(404).json({ error: 'Notation not found' });
126
+ return;
127
+ }
128
+ try {
129
+ const filePath = path_1.default.resolve(projectRoot, notation.location.file);
130
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
131
+ const lines = content.split('\n');
132
+ const startLine = notation.location.line - 1;
133
+ const endLine = (notation.location.endLine ?? notation.location.line) - 1;
134
+ const source = lines.slice(startLine, endLine + 1).join('\n');
135
+ res.json({
136
+ source,
137
+ file: notation.location.file,
138
+ line: notation.location.line,
139
+ endLine: notation.location.endLine ?? notation.location.line,
140
+ });
141
+ }
142
+ catch (err) {
143
+ res.status(500).json({ error: 'Failed to read source file' });
144
+ }
145
+ });
146
+ app.put('/api/notations/:id/source', async (req, res) => {
147
+ const notation = manager.getById(req.params.id);
148
+ if (!notation) {
149
+ res.status(404).json({ error: 'Notation not found' });
150
+ return;
151
+ }
152
+ try {
153
+ const { source } = req.body;
154
+ const filePath = path_1.default.resolve(projectRoot, notation.location.file);
155
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
156
+ const lines = content.split('\n');
157
+ const startLine = notation.location.line - 1;
158
+ const endLine = (notation.location.endLine ?? notation.location.line) - 1;
159
+ const newLines = source.split('\n');
160
+ lines.splice(startLine, endLine - startLine + 1, ...newLines);
161
+ fs_1.default.writeFileSync(filePath, lines.join('\n'), 'utf-8');
162
+ const fresh = await (0, scanner_1.scanFiles)(config, projectRoot);
163
+ manager.setAll(fresh);
164
+ broadcastUpdate();
165
+ res.json({ ok: true });
166
+ }
167
+ catch (err) {
168
+ res.status(500).json({ error: 'Failed to write source file' });
169
+ }
170
+ });
171
+ app.put('/api/notations/:id/actions', async (req, res) => {
172
+ const notation = manager.getById(req.params.id);
173
+ if (!notation) {
174
+ res.status(404).json({ error: 'Notation not found' });
175
+ return;
176
+ }
177
+ try {
178
+ const { actions } = req.body;
179
+ const filePath = path_1.default.resolve(projectRoot, notation.location.file);
180
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
181
+ const lines = content.split('\n');
182
+ const startLine = notation.location.line - 1;
183
+ const endLine = (notation.location.endLine ?? notation.location.line) - 1;
184
+ const blockLines = lines.slice(startLine, endLine + 1);
185
+ // Remove existing Action: lines
186
+ const filtered = blockLines.filter((l) => !l.trim().match(/^\/\/\s*Action:\s*/i));
187
+ // Find where to insert new action lines (after last comment line, before code)
188
+ let insertIdx = 0;
189
+ for (let i = 0; i < filtered.length; i++) {
190
+ if (filtered[i].trim().startsWith('//')) {
191
+ insertIdx = i + 1;
192
+ }
193
+ else {
194
+ break;
195
+ }
196
+ }
197
+ const serialized = (0, action_serializer_1.serializeActions)(actions);
198
+ filtered.splice(insertIdx, 0, ...serialized);
199
+ lines.splice(startLine, endLine - startLine + 1, ...filtered);
200
+ fs_1.default.writeFileSync(filePath, lines.join('\n'), 'utf-8');
201
+ const fresh = await (0, scanner_1.scanFiles)(config, projectRoot);
202
+ manager.setAll(fresh);
203
+ broadcastUpdate();
204
+ res.json({ ok: true });
205
+ }
206
+ catch (err) {
207
+ res.status(500).json({ error: 'Failed to update actions' });
208
+ }
209
+ });
210
+ // --- Snapshot endpoints ---
211
+ app.get('/api/snapshots', async (req, res) => {
212
+ try {
213
+ const days = parseInt(req.query.days) || 30;
214
+ const snapshots = await snapshotStorage.readRange(days);
215
+ res.json(snapshots);
216
+ }
217
+ catch (err) {
218
+ res.status(500).json({ error: 'Failed to read snapshots' });
219
+ }
220
+ });
221
+ app.get('/api/health', (_req, res) => {
222
+ res.json({
223
+ score: currentHealthScore(),
224
+ deductions: computeHealthScoreDetails(manager.getAll()),
225
+ });
226
+ });
227
+ // --- Governance endpoints ---
228
+ app.get('/api/governance/gate', (_req, res) => {
229
+ res.json(currentGateResult());
230
+ });
231
+ app.get('/api/governance/deprecations', (_req, res) => {
232
+ res.json((0, deprecation_tracker_1.computeDeprecationSummary)(manager.getAll()));
233
+ });
234
+ // --- Action execution endpoints ---
235
+ app.post('/api/notations/:id/execute-action', async (req, res) => {
236
+ const notation = manager.getById(req.params.id);
237
+ if (!notation) {
238
+ res.status(404).json({ error: 'Notation not found' });
239
+ return;
240
+ }
241
+ try {
242
+ const { actionIndex } = req.body;
243
+ const action = notation.actions[actionIndex];
244
+ if (!action) {
245
+ res.status(400).json({ error: 'Action not found at index' });
246
+ return;
247
+ }
248
+ const result = await (0, executor_1.executeAction)(action, notation, projectRoot);
249
+ // Rescan after execution
250
+ const fresh = await (0, scanner_1.scanFiles)(config, projectRoot);
251
+ manager.setAll(fresh);
252
+ broadcastUpdate();
253
+ res.json(result);
254
+ }
255
+ catch (err) {
256
+ res.status(500).json({ error: 'Action execution failed' });
257
+ }
258
+ });
259
+ app.put('/api/notations/:id/metadata', async (req, res) => {
260
+ const notation = manager.getById(req.params.id);
261
+ if (!notation) {
262
+ res.status(404).json({ error: 'Notation not found' });
263
+ return;
264
+ }
265
+ try {
266
+ const updates = req.body;
267
+ updateNotationMetadataInSource(notation, updates, projectRoot);
268
+ const fresh = await (0, scanner_1.scanFiles)(config, projectRoot);
269
+ manager.setAll(fresh);
270
+ broadcastUpdate();
271
+ res.json({ ok: true });
272
+ }
273
+ catch (err) {
274
+ res.status(500).json({ error: 'Failed to update metadata' });
275
+ }
276
+ });
277
+ app.post('/api/notations/batch', async (req, res) => {
278
+ try {
279
+ const { ids, operation, payload } = req.body;
280
+ const results = [];
281
+ for (const id of ids) {
282
+ const notation = manager.getById(id);
283
+ if (!notation)
284
+ continue;
285
+ if (operation === 'executeAction') {
286
+ const action = notation.actions[payload.actionIndex];
287
+ if (action) {
288
+ results.push(await (0, executor_1.executeAction)(action, notation, projectRoot));
289
+ }
290
+ }
291
+ else {
292
+ const updates = {};
293
+ if (operation === 'updateStatus')
294
+ updates.status = payload.value;
295
+ if (operation === 'updatePriority')
296
+ updates.priority = payload.value;
297
+ if (operation === 'updateAssignee')
298
+ updates.assignee = payload.value;
299
+ updateNotationMetadataInSource(notation, updates, projectRoot);
300
+ results.push({ ok: true, id });
301
+ }
302
+ }
303
+ const fresh = await (0, scanner_1.scanFiles)(config, projectRoot);
304
+ manager.setAll(fresh);
305
+ broadcastUpdate();
306
+ res.json({ results });
307
+ }
308
+ catch (err) {
309
+ res.status(500).json({ error: 'Batch operation failed' });
310
+ }
311
+ });
312
+ // --- Git blame endpoint ---
313
+ app.get('/api/notations/:id/blame', (req, res) => {
314
+ const notation = manager.getById(req.params.id);
315
+ if (!notation) {
316
+ res.status(404).json({ error: 'Notation not found' });
317
+ return;
318
+ }
319
+ res.json({ blame: notation.blame || null });
320
+ });
321
+ // --- Issue tracker endpoints ---
322
+ app.post('/api/notations/:id/create-issue', async (req, res) => {
323
+ const notation = manager.getById(req.params.id);
324
+ if (!notation) {
325
+ res.status(404).json({ error: 'Notation not found' });
326
+ return;
327
+ }
328
+ try {
329
+ const { provider } = req.body;
330
+ let issueUrl;
331
+ if (provider === 'github') {
332
+ if (!config.integrations.github || !secrets.githubToken) {
333
+ res.status(400).json({ error: 'GitHub integration not configured' });
334
+ return;
335
+ }
336
+ issueUrl = await (0, github_issues_1.createGitHubIssue)(config.integrations.github, secrets.githubToken, notation);
337
+ }
338
+ else if (provider === 'jira') {
339
+ if (!config.integrations.jira || !secrets.jiraToken) {
340
+ res.status(400).json({ error: 'Jira integration not configured' });
341
+ return;
342
+ }
343
+ issueUrl = await (0, jira_issues_1.createJiraIssue)(config.integrations.jira, secrets.jiraToken, notation);
344
+ }
345
+ else {
346
+ res.status(400).json({ error: 'Unknown provider' });
347
+ return;
348
+ }
349
+ // Store the linked issue on the notation
350
+ manager.update(notation.id, { linkedIssue: issueUrl });
351
+ broadcastUpdate();
352
+ res.json({ url: issueUrl });
353
+ }
354
+ catch (err) {
355
+ res.status(500).json({ error: err.message || 'Failed to create issue' });
356
+ }
357
+ });
358
+ // --- AI suggest fix endpoint ---
359
+ app.post('/api/notations/:id/suggest-fix', async (req, res) => {
360
+ const notation = manager.getById(req.params.id);
361
+ if (!notation) {
362
+ res.status(404).json({ error: 'Notation not found' });
363
+ return;
364
+ }
365
+ if (!secrets.aiKey) {
366
+ res.status(400).json({ error: 'AI integration not configured (TRACKER_AI_KEY not set)' });
367
+ return;
368
+ }
369
+ if (notation.type !== 'BUG' && notation.type !== 'OPTIMIZE') {
370
+ res.status(400).json({ error: 'AI fix suggestions only available for BUG and OPTIMIZE notations' });
371
+ return;
372
+ }
373
+ try {
374
+ // Read surrounding source context
375
+ const filePath = path_1.default.resolve(projectRoot, notation.location.file);
376
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
377
+ const lines = content.split('\n');
378
+ const startLine = Math.max(0, notation.location.line - 26);
379
+ const endLine = Math.min(lines.length, (notation.location.endLine ?? notation.location.line) + 25);
380
+ const sourceContext = lines.slice(startLine, endLine).join('\n');
381
+ const model = config.integrations.ai?.model || 'claude-sonnet-4-5';
382
+ const result = await (0, ai_suggest_1.suggestFix)(secrets.aiKey, model, notation, sourceContext);
383
+ res.json(result);
384
+ }
385
+ catch (err) {
386
+ res.status(500).json({ error: err.message || 'AI suggestion failed' });
387
+ }
388
+ });
389
+ // --- Burn-down projection ---
390
+ app.get('/api/projection/burndown', async (_req, res) => {
391
+ try {
392
+ const snapshots = await snapshotStorage.readAll();
393
+ const burndown = (0, projection_1.computeBurnDown)(snapshots, manager.getAll());
394
+ res.json(burndown);
395
+ }
396
+ catch (err) {
397
+ res.status(500).json({ error: 'Failed to compute burn-down' });
398
+ }
399
+ });
76
400
  // --- Static SPA ---
77
401
  const distDashboard = path_1.default.join(__dirname, '..', '..', 'dist-dashboard');
78
402
  app.use(express_1.default.static(distDashboard));
@@ -82,7 +406,7 @@ async function startDashboard(opts) {
82
406
  });
83
407
  // --- HTTP + WebSocket ---
84
408
  const server = http_1.default.createServer(app);
85
- const wss = new ws_1.WebSocketServer({ server });
409
+ const wss = new ws_1.WebSocketServer({ server, path: '/ws' });
86
410
  const clients = new Set();
87
411
  wss.on('connection', (ws) => {
88
412
  clients.add(ws);
@@ -91,6 +415,8 @@ async function startDashboard(opts) {
91
415
  type: 'init',
92
416
  notations: manager.getAll(),
93
417
  stats: manager.stats(),
418
+ healthScore: currentHealthScore(),
419
+ gateResult: currentGateResult(),
94
420
  }));
95
421
  ws.on('close', () => {
96
422
  clients.delete(ws);
@@ -101,6 +427,8 @@ async function startDashboard(opts) {
101
427
  type: 'update',
102
428
  notations: manager.getAll(),
103
429
  stats: manager.stats(),
430
+ healthScore: currentHealthScore(),
431
+ gateResult: currentGateResult(),
104
432
  });
105
433
  for (const client of clients) {
106
434
  if (client.readyState === ws_1.WebSocket.OPEN) {
@@ -109,7 +437,13 @@ async function startDashboard(opts) {
109
437
  }
110
438
  }
111
439
  // --- File Watcher ---
112
- const watcher = (0, watcher_1.createFileWatcher)(config, projectRoot, manager, broadcastUpdate);
440
+ const watcher = (0, watcher_1.createFileWatcher)(config, projectRoot, manager, async () => {
441
+ await takeSnapshotIfNewDay(manager, snapshotStorage);
442
+ if (config.gitBlame) {
443
+ runBlameAsync(projectRoot, manager, () => broadcastUpdate());
444
+ }
445
+ broadcastUpdate();
446
+ });
113
447
  // --- Start ---
114
448
  await new Promise((resolve) => {
115
449
  server.listen(port, () => resolve());
@@ -128,3 +462,132 @@ async function startDashboard(opts) {
128
462
  },
129
463
  };
130
464
  }
465
+ // --- Helpers ---
466
+ async function takeSnapshotIfNewDay(manager, snapshotStorage) {
467
+ const today = new Date().toISOString().slice(0, 10);
468
+ try {
469
+ const latestDate = await snapshotStorage.getLatestDate();
470
+ if (latestDate === today)
471
+ return;
472
+ const stats = manager.stats();
473
+ const healthScore = (0, health_score_1.computeHealthScore)(manager.getAll());
474
+ await snapshotStorage.append({ date: today, stats, healthScore });
475
+ }
476
+ catch (err) {
477
+ console.error('[tracker] Failed to take snapshot:', err);
478
+ }
479
+ }
480
+ function runBlameAsync(projectRoot, manager, onComplete) {
481
+ const notations = manager.getAll();
482
+ (0, git_blame_1.batchBlame)(projectRoot, notations)
483
+ .then((blamed) => {
484
+ for (const n of blamed) {
485
+ if (n.blame) {
486
+ manager.update(n.id, { blame: n.blame });
487
+ }
488
+ }
489
+ onComplete();
490
+ })
491
+ .catch((err) => {
492
+ console.error('[tracker] Git blame failed:', err);
493
+ });
494
+ }
495
+ function computeHealthScoreDetails(notations) {
496
+ const deductions = [];
497
+ let unresolvedHacks = 0;
498
+ let unresolvedBugs = 0;
499
+ let criticalSecurity = 0;
500
+ let highSecurity = 0;
501
+ let overdueCount = 0;
502
+ let blockedCount = 0;
503
+ let pastEol = 0;
504
+ let totalDebt = 0;
505
+ for (const n of notations) {
506
+ if (n.status === 'resolved')
507
+ continue;
508
+ if (n.type === 'HACK')
509
+ unresolvedHacks++;
510
+ if (n.type === 'BUG')
511
+ unresolvedBugs++;
512
+ if (n.type === 'SECURITY' && n.priority === 'critical')
513
+ criticalSecurity++;
514
+ if (n.type === 'SECURITY' && n.priority === 'high')
515
+ highSecurity++;
516
+ if (n.dueDate && new Date(n.dueDate + 'T23:59:59') < new Date())
517
+ overdueCount++;
518
+ if (n.relationships.length > 0)
519
+ blockedCount++;
520
+ if (n.type === 'DEPRECATION' && n.eolDate && new Date(n.eolDate) < new Date())
521
+ pastEol++;
522
+ if (n.debt)
523
+ totalDebt += n.debt.hours;
524
+ }
525
+ if (unresolvedHacks > 0)
526
+ deductions.push({ reason: `${unresolvedHacks} unresolved HACK(s)`, deduction: unresolvedHacks * 3 });
527
+ if (unresolvedBugs > 0)
528
+ deductions.push({ reason: `${unresolvedBugs} unresolved BUG(s)`, deduction: unresolvedBugs * 4 });
529
+ if (criticalSecurity > 0)
530
+ deductions.push({ reason: `${criticalSecurity} critical SECURITY issue(s)`, deduction: criticalSecurity * 10 });
531
+ if (highSecurity > 0)
532
+ deductions.push({ reason: `${highSecurity} high SECURITY issue(s)`, deduction: highSecurity * 5 });
533
+ if (overdueCount > 0)
534
+ deductions.push({ reason: `${overdueCount} overdue notation(s)`, deduction: overdueCount * 2 });
535
+ if (blockedCount > 0)
536
+ deductions.push({ reason: `${blockedCount} blocked notation(s)`, deduction: blockedCount * 1 });
537
+ if (totalDebt > 40)
538
+ deductions.push({ reason: `${totalDebt}h debt (>${40}h threshold)`, deduction: Math.floor((totalDebt - 40) / 10) });
539
+ if (pastEol > 0)
540
+ deductions.push({ reason: `${pastEol} past-EOL deprecation(s)`, deduction: pastEol * 3 });
541
+ return deductions;
542
+ }
543
+ function updateNotationMetadataInSource(notation, updates, projectRoot) {
544
+ const filePath = path_1.default.resolve(projectRoot, notation.location.file);
545
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
546
+ const lines = content.split('\n');
547
+ const startLine = notation.location.line - 1;
548
+ const endLine = (notation.location.endLine ?? notation.location.line) - 1;
549
+ const blockLines = lines.slice(startLine, endLine + 1);
550
+ // Find existing attribute lines or add new ones
551
+ const attrLines = [];
552
+ if (updates.status)
553
+ attrLines.push(`// @status: ${updates.status}`);
554
+ if (updates.priority)
555
+ attrLines.push(`// @priority: ${updates.priority}`);
556
+ if (updates.assignee !== undefined)
557
+ attrLines.push(`// @assignee: ${updates.assignee}`);
558
+ // Find insertion point (after marker line, before code)
559
+ let insertIdx = 1; // After the marker line
560
+ for (let i = 1; i < blockLines.length; i++) {
561
+ if (blockLines[i].trim().startsWith('//')) {
562
+ insertIdx = i + 1;
563
+ }
564
+ else {
565
+ break;
566
+ }
567
+ }
568
+ // Remove existing attribute lines that we're replacing
569
+ const keysToReplace = Object.keys(updates);
570
+ const filtered = blockLines.filter((l, idx) => {
571
+ if (idx === 0)
572
+ return true; // Keep marker line
573
+ for (const key of keysToReplace) {
574
+ const pattern = new RegExp(`^\\s*\\/\\/\\s*@${key}:`, 'i');
575
+ if (pattern.test(l))
576
+ return false;
577
+ }
578
+ return true;
579
+ });
580
+ // Re-find insertion point after filtering
581
+ insertIdx = 1;
582
+ for (let i = 1; i < filtered.length; i++) {
583
+ if (filtered[i].trim().startsWith('//')) {
584
+ insertIdx = i + 1;
585
+ }
586
+ else {
587
+ break;
588
+ }
589
+ }
590
+ filtered.splice(insertIdx, 0, ...attrLines);
591
+ lines.splice(startLine, endLine - startLine + 1, ...filtered);
592
+ fs_1.default.writeFileSync(filePath, lines.join('\n'), 'utf-8');
593
+ }
@@ -3,5 +3,5 @@ import type { NotationManager } from '../manager';
3
3
  export interface FileWatcher {
4
4
  close: () => Promise<void>;
5
5
  }
6
- export declare function createFileWatcher(config: TrackerConfig, projectRoot: string, manager: NotationManager, onUpdate: () => void): FileWatcher;
6
+ export declare function createFileWatcher(config: TrackerConfig, projectRoot: string, manager: NotationManager, onUpdate: () => void | Promise<void>): FileWatcher;
7
7
  //# sourceMappingURL=watcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/dashboard/watcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjD,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,IAAI,GAClB,WAAW,CAqCb"}
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/dashboard/watcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjD,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClC,WAAW,CAqCb"}
@@ -15,7 +15,7 @@ function createFileWatcher(config, projectRoot, manager, onUpdate) {
15
15
  try {
16
16
  const notations = await (0, scanner_1.scanFiles)(config, projectRoot);
17
17
  manager.setAll(notations);
18
- onUpdate();
18
+ await onUpdate();
19
19
  }
20
20
  catch (err) {
21
21
  console.error('[tracker] Re-scan failed:', err);
@@ -1,10 +1,10 @@
1
- import type { NotationAction } from '../types';
1
+ import type { NotationAction, Notation } from '../types';
2
2
  export interface ActionResult {
3
3
  success: boolean;
4
4
  message: string;
5
5
  verb: string;
6
6
  }
7
- export type ActionHandler = (action: NotationAction) => Promise<ActionResult>;
7
+ export type ActionHandler = (action: NotationAction, notation: Notation, projectRoot: string) => Promise<ActionResult>;
8
8
  export declare function registerActionHandler(verb: string, handler: ActionHandler): void;
9
- export declare function executeAction(action: NotationAction): Promise<ActionResult>;
9
+ export declare function executeAction(action: NotationAction, notation: Notation, projectRoot: string): Promise<ActionResult>;
10
10
  //# sourceMappingURL=action-executor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-executor.d.ts","sourceRoot":"","sources":["../../src/executor/action-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAE9C,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;AAI7E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAEhF;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAUjF"}
1
+ {"version":3,"file":"action-executor.d.ts","sourceRoot":"","sources":["../../src/executor/action-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExD,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;AAItH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAEhF;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAU1H"}
@@ -6,7 +6,7 @@ const handlers = new Map();
6
6
  function registerActionHandler(verb, handler) {
7
7
  handlers.set(verb, handler);
8
8
  }
9
- async function executeAction(action) {
9
+ async function executeAction(action, notation, projectRoot) {
10
10
  const handler = handlers.get(action.verb);
11
11
  if (!handler) {
12
12
  return {
@@ -15,5 +15,5 @@ async function executeAction(action) {
15
15
  verb: action.verb,
16
16
  };
17
17
  }
18
- return handler(action);
18
+ return handler(action, notation, projectRoot);
19
19
  }
@@ -0,0 +1,4 @@
1
+ import type { NotationAction, Notation } from '../../types';
2
+ import type { ActionResult } from '../action-executor';
3
+ export declare function handleExtract(action: NotationAction, notation: Notation, projectRoot: string): Promise<ActionResult>;
4
+ //# sourceMappingURL=extract-action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-action.d.ts","sourceRoot":"","sources":["../../../src/executor/actions/extract-action.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAe,MAAM,aAAa,CAAA;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsD1H"}