@imenam/mcp-documentor 1.0.62 → 1.0.63

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.
@@ -65,6 +65,8 @@ export interface DocSummary {
65
65
  updatedAt: string;
66
66
  tags: string[];
67
67
  isStale: boolean;
68
+ /** True when tracked entries have no snapshot and freshness cannot be computed. */
69
+ isBroken?: boolean;
68
70
  folder?: string;
69
71
  }
70
72
  export interface DocMatch {
@@ -81,11 +83,13 @@ export declare const FreshnessReportSchema: z.ZodObject<{
81
83
  stale: "stale";
82
84
  no_tracking: "no_tracking";
83
85
  unverifiable: "unverifiable";
86
+ broken: "broken";
84
87
  }>;
85
88
  modified: z.ZodArray<z.ZodString>;
86
89
  missing: z.ZodArray<z.ZodString>;
87
90
  unchanged: z.ZodArray<z.ZodString>;
88
91
  anchors_lost: z.ZodDefault<z.ZodArray<z.ZodString>>;
92
+ broken: z.ZodDefault<z.ZodArray<z.ZodString>>;
89
93
  total: z.ZodNumber;
90
94
  }, z.core.$strip>;
91
95
  export type FreshnessReport = z.infer<typeof FreshnessReportSchema>;
@@ -36,11 +36,13 @@ export const DocSchema = z.object({
36
36
  updatedAt: z.string(),
37
37
  });
38
38
  export const FreshnessReportSchema = z.object({
39
- status: z.enum(["up_to_date", "stale", "no_tracking", "unverifiable"]),
39
+ status: z.enum(["up_to_date", "stale", "no_tracking", "unverifiable", "broken"]),
40
40
  modified: z.array(z.string()),
41
41
  missing: z.array(z.string()),
42
42
  unchanged: z.array(z.string()),
43
43
  anchors_lost: z.array(z.string()).default([]),
44
+ /** Entries with neither sections nor contentHash (pre hash-based tracking) — cannot be checked until re-tracked. */
45
+ broken: z.array(z.string()).default([]),
44
46
  total: z.number(),
45
47
  });
46
48
  export const CreateDocArgsSchema = z.object({
package/dist/src/index.js CHANGED
@@ -159,15 +159,9 @@ async function handleGetInitialState(msg) {
159
159
  for (const doc of docs) {
160
160
  const full = await storageEngine.getDoc(doc.id); // doc.id is now the full path
161
161
  if (full && full.tracked_files.length > 0) {
162
- // A doc whose freshness cannot be computed (legacy hash-less entries)
163
- // needs attention: surface it as stale rather than crashing the view.
164
- try {
165
- const report = computeFreshness(full.tracked_files, config.currentProject);
166
- doc.isStale = report.status === "stale";
167
- }
168
- catch {
169
- doc.isStale = true;
170
- }
162
+ const report = computeFreshness(full.tracked_files, config.currentProject);
163
+ doc.isStale = report.status === "stale" || report.status === "broken";
164
+ doc.isBroken = report.status === "broken";
171
165
  }
172
166
  }
173
167
  const bundles = storageEngine.getBundles();
@@ -204,13 +198,9 @@ async function handleUpdateProject(msg) {
204
198
  for (const doc of docs) {
205
199
  const full = await storageEngine.getDoc(doc.id);
206
200
  if (full && full.tracked_files.length > 0) {
207
- try {
208
- const report = computeFreshness(full.tracked_files, project);
209
- doc.isStale = report.status === "stale";
210
- }
211
- catch {
212
- doc.isStale = true;
213
- }
201
+ const report = computeFreshness(full.tracked_files, project);
202
+ doc.isStale = report.status === "stale" || report.status === "broken";
203
+ doc.isBroken = report.status === "broken";
214
204
  }
215
205
  }
216
206
  broadcastToGUI({
@@ -250,13 +240,9 @@ async function handleListDocs(msg) {
250
240
  for (const doc of docs) {
251
241
  const full = await storageEngine.getDoc(doc.id);
252
242
  if (full && full.tracked_files.length > 0) {
253
- try {
254
- const report = computeFreshness(full.tracked_files, currentProject);
255
- doc.isStale = report.status === "stale";
256
- }
257
- catch {
258
- doc.isStale = true;
259
- }
243
+ const report = computeFreshness(full.tracked_files, currentProject);
244
+ doc.isStale = report.status === "stale" || report.status === "broken";
245
+ doc.isBroken = report.status === "broken";
260
246
  }
261
247
  }
262
248
  broadcastToGUI({
@@ -368,15 +354,9 @@ async function handleCreateDocIPC(msg) {
368
354
  for (const doc of docs) {
369
355
  const full = await storageEngine.getDoc(doc.id);
370
356
  if (full && full.tracked_files.length > 0) {
371
- // A doc whose freshness cannot be computed (legacy hash-less entries)
372
- // needs attention: surface it as stale rather than crashing the view.
373
- try {
374
- const report = computeFreshness(full.tracked_files, config.currentProject);
375
- doc.isStale = report.status === "stale";
376
- }
377
- catch {
378
- doc.isStale = true;
379
- }
357
+ const report = computeFreshness(full.tracked_files, config.currentProject);
358
+ doc.isStale = report.status === "stale" || report.status === "broken";
359
+ doc.isBroken = report.status === "broken";
380
360
  }
381
361
  }
382
362
  broadcastToGUI({
@@ -8,12 +8,13 @@ export function computeFileHash(absolutePath) {
8
8
  }
9
9
  export function computeFreshness(trackedFiles, project) {
10
10
  if (!trackedFiles || trackedFiles.length === 0) {
11
- return { status: "no_tracking", modified: [], missing: [], unchanged: [], anchors_lost: [], total: 0 };
11
+ return { status: "no_tracking", modified: [], missing: [], unchanged: [], anchors_lost: [], broken: [], total: 0 };
12
12
  }
13
13
  const modified = [];
14
14
  const missing = [];
15
15
  const unchanged = [];
16
16
  const anchors_lost = [];
17
+ const broken = [];
17
18
  for (const tracked of trackedFiles) {
18
19
  // A relative path with no code root configured on this machine is not
19
20
  // resolvable here — count it as absent so we end up "unverifiable" rather
@@ -61,8 +62,10 @@ export function computeFreshness(trackedFiles, project) {
61
62
  }
62
63
  else {
63
64
  // Hash comparison is the only freshness system. An entry with neither
64
- // sections nor contentHash predates it and cannot be checked.
65
- throw new Error(`Tracked file "${tracked.path}" has no contentHash and no sections — it predates hash-based tracking. Call refresh_file_snapshot on this doc to snapshot it, or re-declare it via update_doc.`);
65
+ // sections nor contentHash predates it and cannot be checked — it is
66
+ // reported as broken (never checked by any other means) so readers can
67
+ // still expose the doc and its tracked list to the agent for re-tracking.
68
+ broken.push(tracked.path);
66
69
  }
67
70
  }
68
71
  // When every tracked file is absent, the source code simply isn't present on
@@ -72,15 +75,18 @@ export function computeFreshness(trackedFiles, project) {
72
75
  // change signal (likely deletions) and still counts as stale.
73
76
  const allMissing = missing.length === trackedFiles.length;
74
77
  if (allMissing) {
75
- return { status: "unverifiable", modified, missing, unchanged, anchors_lost, total: trackedFiles.length };
78
+ return { status: "unverifiable", modified, missing, unchanged, anchors_lost, broken, total: trackedFiles.length };
76
79
  }
77
80
  const isStale = modified.length > 0 || missing.length > 0 || anchors_lost.length > 0;
81
+ // Broken tracking outranks everything: no freshness verdict is trustworthy
82
+ // until the broken entries are re-tracked.
78
83
  return {
79
- status: isStale ? "stale" : "up_to_date",
84
+ status: broken.length > 0 ? "broken" : isStale ? "stale" : "up_to_date",
80
85
  modified,
81
86
  missing,
82
87
  unchanged,
83
88
  anchors_lost,
89
+ broken,
84
90
  total: trackedFiles.length,
85
91
  };
86
92
  }
@@ -91,11 +97,23 @@ export function formatFreshnessReport(title, report) {
91
97
  if (report.status === "unverifiable") {
92
98
  return `UNVERIFIABLE: "${title}" — none of the ${report.total} tracked file(s) are present on this machine, so freshness cannot be checked here. This is expected when the documentation is cloned without its source codebase.`;
93
99
  }
94
- let text = report.status === "stale"
95
- ? `STALE: "${title}" — tracked files have changed since the documentation was last updated.\n`
96
- : `UP TO DATE: "${title}" — no tracked files have been modified.\n`;
100
+ let text;
101
+ if (report.status === "broken") {
102
+ text = `BROKEN TRACKING: "${title}" — some tracked entries have no content snapshot and cannot be checked.\n`;
103
+ }
104
+ else if (report.status === "stale") {
105
+ text = `STALE: "${title}" — tracked files have changed since the documentation was last updated.\n`;
106
+ }
107
+ else {
108
+ text = `UP TO DATE: "${title}" — no tracked files have been modified.\n`;
109
+ }
97
110
  const anchorLostCount = report.anchors_lost?.length ?? 0;
98
- text += `\nSummary: ${report.modified.length} modified, ${report.missing.length} missing, ${anchorLostCount} with lost anchors, ${report.unchanged.length} unchanged out of ${report.total} tracked file(s).`;
111
+ const brokenCount = report.broken?.length ?? 0;
112
+ text += `\nSummary: ${report.modified.length} modified, ${report.missing.length} missing, ${anchorLostCount} with lost anchors, ${brokenCount} with broken tracking, ${report.unchanged.length} unchanged out of ${report.total} tracked file(s).`;
113
+ if (brokenCount > 0) {
114
+ text += `\n\n⚠ Files with broken tracking — no snapshot to compare against:\n${report.broken.map((f) => ` - ${f}`).join("\n")}`;
115
+ text += `\n → These entries predate hash-based tracking. Re-read the source files and re-declare them in tracked_files via update_doc, or call refresh_file_snapshot if the doc content is still accurate.`;
116
+ }
99
117
  if (report.modified.length > 0) {
100
118
  text += `\n\nModified files (call refresh_file_snapshot to acknowledge):\n${report.modified.map((f) => ` - ${f}`).join("\n")}`;
101
119
  }
@@ -125,16 +143,12 @@ async function scanProjectFreshness(storageEngine, project) {
125
143
  const doc = await storageEngine.getDoc(summary.id, project);
126
144
  if (!doc)
127
145
  continue;
128
- let report;
129
- try {
130
- report = computeFreshness(doc.tracked_files, project);
131
- }
132
- catch (err) {
133
- const message = err instanceof Error ? err.message : String(err);
134
- scan.broken.push(` - **${doc.title}** [${doc.id}]: ${message}`);
135
- continue;
146
+ const report = computeFreshness(doc.tracked_files, project);
147
+ if (report.status === "broken") {
148
+ const files = report.broken.map((f) => ` • no snapshot: ${f}`).join("\n");
149
+ scan.broken.push(` - **${doc.title}** [${doc.id}]\n${files}`);
136
150
  }
137
- if (report.status === "no_tracking") {
151
+ else if (report.status === "no_tracking") {
138
152
  scan.noTracking.push(doc.title);
139
153
  }
140
154
  else if (report.status === "unverifiable") {
@@ -13,6 +13,8 @@ export async function getDocMetadata(storageEngine, args) {
13
13
  const { doc, project } = resolved;
14
14
  const report = computeFreshness(doc.tracked_files, project);
15
15
  const fileStatusIcon = (filePath) => {
16
+ if (report.broken.includes(filePath))
17
+ return "⚠ broken tracking (no snapshot — re-declare via update_doc or run refresh_file_snapshot)";
16
18
  if (report.modified.includes(filePath))
17
19
  return "✗ modified";
18
20
  if (report.missing.includes(filePath))
@@ -29,8 +29,20 @@ export async function getDoc(storageEngine, args) {
29
29
  }).join("\n")
30
30
  : " (none)";
31
31
  const freshnessReport = computeFreshness(doc.tracked_files, resolvedProject);
32
- const stalenessBanner = freshnessReport.status === "stale"
33
- ? [
32
+ let stalenessBanner = "";
33
+ if (freshnessReport.status === "broken") {
34
+ stalenessBanner = [
35
+ `> [!WARNING]`,
36
+ `> **Le suivi de fichiers de cette documentation est cassé.**`,
37
+ `> Les entrées suivantes n'ont aucun snapshot de contenu et ne peuvent pas être vérifiées :`,
38
+ ...freshnessReport.broken.map((f) => `> - \`${f}\` *(aucun snapshot)*`),
39
+ `>`,
40
+ `> La fraîcheur de ce document est invérifiable tant que ces entrées ne sont pas re-suivies. Relisez les fichiers sources et re-déclarez-les via \`update_doc\` (tracked_files), ou appelez \`refresh_file_snapshot\` si le contenu ci-dessous est encore exact.`,
41
+ ``,
42
+ ].join("\n");
43
+ }
44
+ else if (freshnessReport.status === "stale") {
45
+ stalenessBanner = [
34
46
  `> [!WARNING]`,
35
47
  `> **Cette documentation est potentiellement obsolète.**`,
36
48
  `> Les fichiers suivants ont été modifiés depuis la dernière mise à jour de ce document :`,
@@ -41,8 +53,8 @@ export async function getDoc(storageEngine, args) {
41
53
  `>`,
42
54
  `> Vérifiez si le contenu ci-dessous est toujours valide. Si nécessaire, utilisez \`git_diff_doc\` pour voir les changements, puis mettez à jour la documentation avec \`patch_doc\` ou \`update_doc\`.`,
43
55
  ``,
44
- ].join("\n")
45
- : "";
56
+ ].join("\n");
57
+ }
46
58
  const text = [
47
59
  stalenessBanner,
48
60
  `# ${doc.title}`,
@@ -62,7 +62,9 @@ export async function gitDiffDoc(storageEngine, args) {
62
62
  }
63
63
  else {
64
64
  const freshness = computeFreshness(doc.tracked_files, project);
65
- const changedPaths = new Set([...freshness.modified, ...freshness.missing]);
65
+ // Broken entries (no snapshot) can't be hash-compared, but the git diff
66
+ // is still the best signal the agent has to re-assess them — include them.
67
+ const changedPaths = new Set([...freshness.modified, ...freshness.missing, ...freshness.broken]);
66
68
  filesToDiff = doc.tracked_files.filter((tf) => changedPaths.has(tf.path));
67
69
  }
68
70
  if (filesToDiff.length === 0) {
@@ -21,7 +21,7 @@ function renderProjectDocs(summaries, totalLabel) {
21
21
  if (key)
22
22
  lines.push(`\n 📁 ${key}`);
23
23
  for (const s of groups.get(key)) {
24
- const staleTag = s.isStale ? " [STALE]" : "";
24
+ const staleTag = s.isBroken ? " [BROKEN TRACKING]" : s.isStale ? " [STALE]" : "";
25
25
  const tags = s.tags.length > 0 ? ` (${s.tags.join(", ")})` : "";
26
26
  const indent = key ? " " : " ";
27
27
  lines.push(`${indent}- ${s.title} [${s.id}]${tags}${staleTag} — updated ${s.updatedAt}`);
@@ -43,6 +43,7 @@ export async function listDocs(storageEngine, args) {
43
43
  if (doc && doc.tracked_files.length > 0) {
44
44
  const report = computeFreshness(doc.tracked_files, proj);
45
45
  summary.isStale = report.status === "stale";
46
+ summary.isBroken = report.status === "broken";
46
47
  }
47
48
  }
48
49
  if (summaries.length === 0) {
@@ -61,6 +62,7 @@ export async function listDocs(storageEngine, args) {
61
62
  if (doc && doc.tracked_files.length > 0) {
62
63
  const report = computeFreshness(doc.tracked_files, proj);
63
64
  summary.isStale = report.status === "stale";
65
+ summary.isBroken = report.status === "broken";
64
66
  }
65
67
  }
66
68
  grandTotal += summaries.length;
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.62";
1
+ export declare const APP_VERSION = "1.0.63";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.62';
2
+ export const APP_VERSION = '1.0.63';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-documentor",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",