@imenam/mcp-documentor 1.0.62 → 1.0.64
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/dist/shared/schemas/doc.schema.d.ts +4 -0
- package/dist/shared/schemas/doc.schema.js +3 -1
- package/dist/src/index.js +91 -33
- package/dist/src/tools/check-freshness.js +32 -18
- package/dist/src/tools/get-doc-metadata.js +2 -0
- package/dist/src/tools/get-doc.js +16 -4
- package/dist/src/tools/git-diff-doc.js +3 -1
- package/dist/src/tools/list-docs.js +3 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
@@ -81,6 +81,9 @@ storageEngine.setHistoryStore(historyStore);
|
|
|
81
81
|
const syncEngine = new SyncEngine(storageEngine);
|
|
82
82
|
const server = new Server({ name: "mcp-documentor", version: APP_VERSION }, { capabilities: { tools: {} } });
|
|
83
83
|
let guiLauncher;
|
|
84
|
+
let guiStatus = "disabled";
|
|
85
|
+
let guiUrl = null;
|
|
86
|
+
let guiStartWaiter = null;
|
|
84
87
|
const sseControllers = new Set();
|
|
85
88
|
function broadcastToGUI(message) {
|
|
86
89
|
const child = guiLauncher.getProcess();
|
|
@@ -95,9 +98,15 @@ function broadcastToGUI(message) {
|
|
|
95
98
|
}
|
|
96
99
|
function handleIPCMessage(msg) {
|
|
97
100
|
if (msg.type === "ALREADY_RUNNING") {
|
|
101
|
+
guiStatus = "already_running";
|
|
102
|
+
guiUrl = msg.data?.url ?? guiUrl;
|
|
103
|
+
guiStartWaiter?.({ outcome: "already_running", url: msg.data?.url });
|
|
98
104
|
logger.info(`GUI is already running at: ${msg.data?.url || "unknown URL"}.`);
|
|
99
105
|
}
|
|
100
106
|
else if (msg.type === "READY") {
|
|
107
|
+
guiStatus = "ready";
|
|
108
|
+
guiUrl = msg.data?.url ?? null;
|
|
109
|
+
guiStartWaiter?.({ outcome: "ready", url: msg.data?.url });
|
|
101
110
|
logger.info(`GUI is ready! Access it at: ${msg.data.url}`);
|
|
102
111
|
}
|
|
103
112
|
else if (msg.type === "GET_INITIAL_STATE") {
|
|
@@ -159,15 +168,9 @@ async function handleGetInitialState(msg) {
|
|
|
159
168
|
for (const doc of docs) {
|
|
160
169
|
const full = await storageEngine.getDoc(doc.id); // doc.id is now the full path
|
|
161
170
|
if (full && full.tracked_files.length > 0) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
166
|
-
doc.isStale = report.status === "stale";
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
doc.isStale = true;
|
|
170
|
-
}
|
|
171
|
+
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
172
|
+
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
173
|
+
doc.isBroken = report.status === "broken";
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
const bundles = storageEngine.getBundles();
|
|
@@ -204,13 +207,9 @@ async function handleUpdateProject(msg) {
|
|
|
204
207
|
for (const doc of docs) {
|
|
205
208
|
const full = await storageEngine.getDoc(doc.id);
|
|
206
209
|
if (full && full.tracked_files.length > 0) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
doc.isStale = true;
|
|
213
|
-
}
|
|
210
|
+
const report = computeFreshness(full.tracked_files, project);
|
|
211
|
+
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
212
|
+
doc.isBroken = report.status === "broken";
|
|
214
213
|
}
|
|
215
214
|
}
|
|
216
215
|
broadcastToGUI({
|
|
@@ -250,13 +249,9 @@ async function handleListDocs(msg) {
|
|
|
250
249
|
for (const doc of docs) {
|
|
251
250
|
const full = await storageEngine.getDoc(doc.id);
|
|
252
251
|
if (full && full.tracked_files.length > 0) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
doc.isStale = true;
|
|
259
|
-
}
|
|
252
|
+
const report = computeFreshness(full.tracked_files, currentProject);
|
|
253
|
+
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
254
|
+
doc.isBroken = report.status === "broken";
|
|
260
255
|
}
|
|
261
256
|
}
|
|
262
257
|
broadcastToGUI({
|
|
@@ -368,15 +363,9 @@ async function handleCreateDocIPC(msg) {
|
|
|
368
363
|
for (const doc of docs) {
|
|
369
364
|
const full = await storageEngine.getDoc(doc.id);
|
|
370
365
|
if (full && full.tracked_files.length > 0) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
375
|
-
doc.isStale = report.status === "stale";
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
doc.isStale = true;
|
|
379
|
-
}
|
|
366
|
+
const report = computeFreshness(full.tracked_files, config.currentProject);
|
|
367
|
+
doc.isStale = report.status === "stale" || report.status === "broken";
|
|
368
|
+
doc.isBroken = report.status === "broken";
|
|
380
369
|
}
|
|
381
370
|
}
|
|
382
371
|
broadcastToGUI({
|
|
@@ -661,13 +650,70 @@ guiLauncher = new GuiLauncher({
|
|
|
661
650
|
handleIPCMessage(msg);
|
|
662
651
|
},
|
|
663
652
|
});
|
|
653
|
+
function startGuiWorker() {
|
|
654
|
+
guiStatus = "starting";
|
|
655
|
+
const child = guiLauncher.start();
|
|
656
|
+
child.on("exit", () => {
|
|
657
|
+
// Un start() ultérieur remplace le process courant : seul l'exit du process
|
|
658
|
+
// actif change l'état. ALREADY_RUNNING = la GUI vit dans une autre instance,
|
|
659
|
+
// l'exit du worker est alors attendu.
|
|
660
|
+
if (guiLauncher.getProcess() === child && guiStatus !== "already_running") {
|
|
661
|
+
guiStatus = "stopped";
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
return child;
|
|
665
|
+
}
|
|
664
666
|
function launchGUI() {
|
|
665
667
|
if (!process.env.PROXY_URL) {
|
|
666
668
|
logger.info('[MASTER] PROXY_URL non défini — GUI désactivée.');
|
|
669
|
+
guiStatus = "disabled";
|
|
667
670
|
return;
|
|
668
671
|
}
|
|
669
672
|
logger.info("Launching GUI worker...");
|
|
670
|
-
|
|
673
|
+
startGuiWorker();
|
|
674
|
+
}
|
|
675
|
+
async function reconnectGui(args) {
|
|
676
|
+
getConfig(); // relit le .env au cas où PROXY_URL aurait été ajouté après le démarrage
|
|
677
|
+
if (!process.env.PROXY_URL) {
|
|
678
|
+
return {
|
|
679
|
+
content: [{ type: "text", text: "Error: PROXY_URL is not set — the GUI is disabled. Add PROXY_URL to the .env file, then call reconnect_gui again." }],
|
|
680
|
+
isError: true,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const child = guiLauncher.getProcess();
|
|
684
|
+
const alive = !!child && child.exitCode === null && !child.killed;
|
|
685
|
+
const force = args?.force === true;
|
|
686
|
+
if (alive && !force) {
|
|
687
|
+
if (guiStatus === "starting") {
|
|
688
|
+
return { content: [{ type: "text", text: "GUI worker is already starting (proxy registration in progress). Wait a few seconds, then call reconnect_gui again if needed." }] };
|
|
689
|
+
}
|
|
690
|
+
return { content: [{ type: "text", text: `GUI is already registered with the proxy${guiUrl ? ` at ${guiUrl}` : ""}. Nothing to do (use force=true to restart it anyway).` }] };
|
|
691
|
+
}
|
|
692
|
+
logger.info(`[MASTER] reconnect_gui: ${alive ? "restarting (force)" : "relaunching"} GUI worker...`);
|
|
693
|
+
const result = await new Promise((resolve) => {
|
|
694
|
+
const newChild = startGuiWorker();
|
|
695
|
+
const onExit = (code) => { cleanup(); resolve({ outcome: "exited", code }); };
|
|
696
|
+
const timer = setTimeout(() => { cleanup(); resolve({ outcome: "timeout" }); }, 15000);
|
|
697
|
+
const cleanup = () => { clearTimeout(timer); newChild.off("exit", onExit); guiStartWaiter = null; };
|
|
698
|
+
guiStartWaiter = (r) => { cleanup(); resolve(r); };
|
|
699
|
+
newChild.on("exit", onExit);
|
|
700
|
+
});
|
|
701
|
+
if (result.outcome === "ready") {
|
|
702
|
+
return { content: [{ type: "text", text: `GUI reconnected and registered with the proxy. URL: ${result.url}` }] };
|
|
703
|
+
}
|
|
704
|
+
if (result.outcome === "already_running") {
|
|
705
|
+
return { content: [{ type: "text", text: `GUI is already registered by another MCP instance. URL: ${result.url ?? "unknown"}` }] };
|
|
706
|
+
}
|
|
707
|
+
if (result.outcome === "exited") {
|
|
708
|
+
return {
|
|
709
|
+
content: [{ type: "text", text: `GUI worker exited before completing registration (code=${result.code}). The proxy server is probably still unreachable at ${process.env.PROXY_URL}. Start the proxy server, then call reconnect_gui again.` }],
|
|
710
|
+
isError: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
content: [{ type: "text", text: "GUI worker started but did not confirm proxy registration within 15s. Check the logs (.docs-data/logs), then retry." }],
|
|
715
|
+
isError: true,
|
|
716
|
+
};
|
|
671
717
|
}
|
|
672
718
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
673
719
|
const tools = [
|
|
@@ -1103,6 +1149,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1103
1149
|
required: ["strategy"],
|
|
1104
1150
|
},
|
|
1105
1151
|
},
|
|
1152
|
+
{
|
|
1153
|
+
name: "reconnect_gui",
|
|
1154
|
+
description: "Relaunch the GUI worker and re-register it with the proxy server. Use this when the proxy server was not running when the MCP server started (so the GUI registration failed), to redeploy the GUI without restarting the MCP server. Does nothing if the GUI is already registered, unless force=true.",
|
|
1155
|
+
inputSchema: {
|
|
1156
|
+
type: "object",
|
|
1157
|
+
properties: {
|
|
1158
|
+
force: { type: "boolean", description: "Restart the GUI worker even if it is already running and registered (default: false)" },
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1106
1162
|
];
|
|
1107
1163
|
return {
|
|
1108
1164
|
tools: isReadOnly() ? tools.filter((t) => !WRITE_TOOLS.has(t.name)) : tools,
|
|
@@ -1194,6 +1250,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1194
1250
|
return await syncDocs(syncEngine, storageEngine, embeddingEngine, embeddingStore, args);
|
|
1195
1251
|
case "resolve_sync_conflict":
|
|
1196
1252
|
return await resolveSyncConflict(syncEngine, args);
|
|
1253
|
+
case "reconnect_gui":
|
|
1254
|
+
return await reconnectGui(args);
|
|
1197
1255
|
default:
|
|
1198
1256
|
throw new Error(`Tool not found: ${name}`);
|
|
1199
1257
|
}
|
|
@@ -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
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.64";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated version file - do not edit manually
|
|
2
|
-
export const APP_VERSION = '1.0.
|
|
2
|
+
export const APP_VERSION = '1.0.64';
|