@imenam/mcp-documentor 1.0.78 → 1.0.80
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/src/core/concurrency.d.ts +12 -0
- package/dist/src/core/concurrency.js +27 -0
- package/dist/src/core/github-code-api.d.ts +6 -1
- package/dist/src/core/github-code-api.js +40 -27
- package/dist/src/core/storage-engine.d.ts +15 -1
- package/dist/src/core/storage-engine.js +24 -10
- package/dist/src/index.js +11 -11
- package/dist/src/tools/check-freshness.d.ts +6 -0
- package/dist/src/tools/check-freshness.js +63 -83
- package/dist/src/tools/list-docs.js +18 -19
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/{arc-CsM5OJTM.js → arc-CdXRzMBT.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BwIu1llT.js → architectureDiagram-3BPJPVTR-CGUnwe_I.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-BVijVnPI.js → blockDiagram-GPEHLZMM-BCI5_vQE.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-D-rcZUZu.js → c4Diagram-AAUBKEIU-Bm9a6HGg.js} +1 -1
- package/gui/dist/assets/channel-f0BgDIcS.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-ByfTGREe.js → chunk-2J33WTMH-c7xNwExL.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-B_5oCXyz.js → chunk-4BX2VUAB-CmcU4Q0o.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-Bc2Kq9HD.js → chunk-55IACEB6-K8rRjNKN.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-CVwmKEMQ.js → chunk-727SXJPM-DMPnL1bv.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-BkMuEFxD.js → chunk-AQP2D5EJ-BeRIIft8.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-CU7EDl9I.js → chunk-FMBD7UC4-CN4It-fp.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-1b34Dtkn.js → chunk-ND2GUHAM-Dy-ZqPua.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-DnUfMecI.js → chunk-QZHKN3VN-CaV4seJw.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-tGvt4n1B.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-tGvt4n1B.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-D9OBrxKr.js → cose-bilkent-S5V4N54A-B1qt_j1Z.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-zDqwL8xs.js → dagre-BM42HDAG-4x5JEBQG.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-ejbP8WIj.js → diagram-2AECGRRQ-Q7TvjRjv.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-Cv9WmtFO.js → diagram-5GNKFQAL-Dguj26bf.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-BI0ybJiN.js → diagram-KO2AKTUF-DBv6MoZF.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-DlVGttLq.js → diagram-LMA3HP47-mfVXwL5q.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-DjoAH-Fq.js → diagram-OG6HWLK6-Bn3_yQWG.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-BVQHVGK4.js → erDiagram-TEJ5UH35-Bp4wnBw7.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-oZP2Z-6T.js → flowDiagram-I6XJVG4X-CGk72E1Q.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-dy4QyEOP.js → ganttDiagram-6RSMTGT7-C1WQIgpu.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-BxgLy7Ln.js → gitGraphDiagram-PVQCEYII-DSIs1ZqI.js} +1 -1
- package/gui/dist/assets/{index-Dtv0slFQ.js → index-T7ZLHNgs.js} +4 -4
- package/gui/dist/assets/{infoDiagram-5YYISTIA-DNOdinpR.js → infoDiagram-5YYISTIA-DzHdfk5Y.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-C4pQ434G.js → ishikawaDiagram-YF4QCWOH-DLxqTmDU.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-CBrVYW94.js → journeyDiagram-JHISSGLW-DYUPmZnf.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-Df-rgiY8.js → kanban-definition-UN3LZRKU-f6km2MDT.js} +1 -1
- package/gui/dist/assets/{linear-B_6fRXVt.js → linear-DHhAaAXH.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-CRAd_VDM.js → mindmap-definition-RKZ34NQL-nergVxV3.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-CJkmLrDI.js → pieDiagram-4H26LBE5-D4IJAdGz.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-D-rDEcnF.js → quadrantDiagram-W4KKPZXB-B5yYdOsV.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-DE8MhSMe.js → requirementDiagram-4Y6WPE33-BrhJp07G.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-BwlxtrUs.js → sankeyDiagram-5OEKKPKP-CUBA4e7F.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-B4LjBzWg.js → sequenceDiagram-3UESZ5HK-DP0-rUie.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-Bv72afPK.js → stateDiagram-AJRCARHV-_BnVcpam.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-B5Vwq56z.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-CBxgf8iW.js → timeline-definition-PNZ67QCA-crbYjsrq.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-DPSdK06q.js → vennDiagram-CIIHVFJN-EOqENtE1.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-JIAzuoPm.js → wardley-L42UT6IY-B1FVq-_Y.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-YDfaF1wT.js → wardleyDiagram-YWT4CUSO-D1DZwIH_.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DHlPryZX.js → xychartDiagram-2RQKCTM6-B6Wr8_OJ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/gui/dist/assets/channel-DKPwwuQf.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-CnCvVGW9.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CnCvVGW9.js +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BKJDOkkR.js +0 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps `items` with at most `limit` calls in flight, preserving input order in the result.
|
|
3
|
+
*
|
|
4
|
+
* Freshness work is almost entirely waiting on the network: one round trip per file that has to
|
|
5
|
+
* be read. Running those sequentially makes a whole-project scan take latency × file count, which
|
|
6
|
+
* is what pushes a large project past the MCP client's timeout. Order is preserved because the
|
|
7
|
+
* reports list files in the order the doc declares them.
|
|
8
|
+
*
|
|
9
|
+
* A rejection propagates once every worker has settled; callers that need per-item failures
|
|
10
|
+
* (freshness does) catch inside `fn` and return a value instead.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps `items` with at most `limit` calls in flight, preserving input order in the result.
|
|
3
|
+
*
|
|
4
|
+
* Freshness work is almost entirely waiting on the network: one round trip per file that has to
|
|
5
|
+
* be read. Running those sequentially makes a whole-project scan take latency × file count, which
|
|
6
|
+
* is what pushes a large project past the MCP client's timeout. Order is preserved because the
|
|
7
|
+
* reports list files in the order the doc declares them.
|
|
8
|
+
*
|
|
9
|
+
* A rejection propagates once every worker has settled; callers that need per-item failures
|
|
10
|
+
* (freshness does) catch inside `fn` and return a value instead.
|
|
11
|
+
*/
|
|
12
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
13
|
+
if (items.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const results = new Array(items.length);
|
|
16
|
+
let next = 0;
|
|
17
|
+
const worker = async () => {
|
|
18
|
+
while (true) {
|
|
19
|
+
const index = next++;
|
|
20
|
+
if (index >= items.length)
|
|
21
|
+
return;
|
|
22
|
+
results[index] = await fn(items[index], index);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
@@ -52,8 +52,13 @@ export declare class GitHubCodeApi {
|
|
|
52
52
|
* once within it, however many files that operation ends up reading.
|
|
53
53
|
*/
|
|
54
54
|
resolveRef(): Promise<ResolvedRef>;
|
|
55
|
-
/** path → blob sha for every file in the commit's tree. */
|
|
55
|
+
/** path → blob sha for every file in the commit's tree. A commit's tree never changes. */
|
|
56
56
|
getTree(commitSha: string): Promise<Map<string, string>>;
|
|
57
|
+
/**
|
|
58
|
+
* Memoises `compute` under `key`, sharing the in-flight promise with concurrent callers.
|
|
59
|
+
* A failure is evicted, so a transient network error does not poison the cache for good.
|
|
60
|
+
*/
|
|
61
|
+
private cached;
|
|
57
62
|
/** Whether the tree we hold for this commit is known to be incomplete. */
|
|
58
63
|
isTreeTruncated(commitSha: string): boolean;
|
|
59
64
|
/** Blob sha for one path at one commit, or undefined if the file does not exist there. */
|
|
@@ -54,6 +54,9 @@ export class GitHubApiError extends Error {
|
|
|
54
54
|
export class GitHubCodeApi {
|
|
55
55
|
spec;
|
|
56
56
|
token;
|
|
57
|
+
// In-flight requests are cached, not just their results: a whole-project scan checks many docs
|
|
58
|
+
// at once, and they all need the same tree — and often the same blobs. Caching the promise makes
|
|
59
|
+
// concurrent callers await one request instead of each firing its own.
|
|
57
60
|
treeByCommit = new Map();
|
|
58
61
|
truncatedCommits = new Set();
|
|
59
62
|
blobCache = new Map();
|
|
@@ -113,25 +116,38 @@ export class GitHubCodeApi {
|
|
|
113
116
|
};
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
|
-
/** path → blob sha for every file in the commit's tree. */
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
/** path → blob sha for every file in the commit's tree. A commit's tree never changes. */
|
|
120
|
+
getTree(commitSha) {
|
|
121
|
+
return this.cached(this.treeByCommit, commitSha, async () => {
|
|
122
|
+
const tree = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/git/trees/${commitSha}?recursive=1`);
|
|
123
|
+
const map = new Map();
|
|
124
|
+
for (const entry of tree.tree ?? []) {
|
|
125
|
+
if (entry.type === "blob")
|
|
126
|
+
map.set(entry.path, entry.sha);
|
|
127
|
+
}
|
|
128
|
+
if (tree.truncated) {
|
|
129
|
+
// Past ~100k entries GitHub stops listing. Existence checks would then be wrong, so we
|
|
130
|
+
// remember it and fall back to a per-path lookup for anything absent from the partial tree.
|
|
131
|
+
this.truncatedCommits.add(commitSha);
|
|
132
|
+
logger.warn(`${this.label}: tree listing truncated at ${map.size} entries; falling back to per-path lookups`);
|
|
133
|
+
}
|
|
134
|
+
return map;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Memoises `compute` under `key`, sharing the in-flight promise with concurrent callers.
|
|
139
|
+
* A failure is evicted, so a transient network error does not poison the cache for good.
|
|
140
|
+
*/
|
|
141
|
+
cached(store, key, compute) {
|
|
142
|
+
const pending = store.get(key);
|
|
143
|
+
if (pending)
|
|
144
|
+
return pending;
|
|
145
|
+
const promise = compute().catch((err) => {
|
|
146
|
+
store.delete(key);
|
|
147
|
+
throw err;
|
|
148
|
+
});
|
|
149
|
+
store.set(key, promise);
|
|
150
|
+
return promise;
|
|
135
151
|
}
|
|
136
152
|
/** Whether the tree we hold for this commit is known to be incomplete. */
|
|
137
153
|
isTreeTruncated(commitSha) {
|
|
@@ -154,14 +170,11 @@ export class GitHubCodeApi {
|
|
|
154
170
|
}
|
|
155
171
|
}
|
|
156
172
|
/** A blob's bytes. Blobs are immutable, so this is cached for the life of the process. */
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
const bytes = Buffer.from(blob.content ?? "", blob.encoding === "base64" ? "base64" : "utf-8");
|
|
163
|
-
this.blobCache.set(blobSha, bytes);
|
|
164
|
-
return bytes;
|
|
173
|
+
getBlob(blobSha) {
|
|
174
|
+
return this.cached(this.blobCache, blobSha, async () => {
|
|
175
|
+
const blob = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/git/blobs/${blobSha}`);
|
|
176
|
+
return Buffer.from(blob.content ?? "", blob.encoding === "base64" ? "base64" : "utf-8");
|
|
177
|
+
});
|
|
165
178
|
}
|
|
166
179
|
/**
|
|
167
180
|
* Unified diff of every file that changed between two commits, keyed by path.
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type { DocEntry, DocSummary, ExportBundle } from "../../shared/schemas/doc.schema.js";
|
|
1
|
+
import type { DocEntry, DocSummary, TrackedFile, ExportBundle } from "../../shared/schemas/doc.schema.js";
|
|
2
2
|
import type { HistoryStore } from "./history-store.js";
|
|
3
3
|
interface GlobalConfig {
|
|
4
4
|
projects: string[];
|
|
5
5
|
foldersMigrated?: boolean;
|
|
6
6
|
}
|
|
7
|
+
/** One doc as a project listing yields it: the GUI-facing summary, plus what it tracks. */
|
|
8
|
+
export interface DocListing {
|
|
9
|
+
summary: DocSummary;
|
|
10
|
+
tracked_files: TrackedFile[];
|
|
11
|
+
}
|
|
7
12
|
/**
|
|
8
13
|
* GitHub sync settings. Sourced exclusively from environment variables — the
|
|
9
14
|
* repository, branch and token are administration concerns and must never be
|
|
@@ -43,6 +48,15 @@ export declare class StorageEngine {
|
|
|
43
48
|
saveDoc(doc: DocEntry, project: string, changeType?: "create" | "update" | "patch"): Promise<void>;
|
|
44
49
|
getDoc(id: string, project: string): Promise<DocEntry | null>;
|
|
45
50
|
listDocs(project: string): Promise<DocSummary[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Same listing, plus the tracked files parsed from the very same read.
|
|
53
|
+
*
|
|
54
|
+
* Freshness has to know what each doc tracks, and fetching that with `getDoc` per summary read
|
|
55
|
+
* and re-parsed every file of the project a second time. Callers that scan a whole project use
|
|
56
|
+
* this instead; `tracked_files` stays out of `DocSummary` so it is never shipped to the GUI,
|
|
57
|
+
* which only needs the stale/broken flags.
|
|
58
|
+
*/
|
|
59
|
+
listDocsDetailed(project: string): Promise<DocListing[]>;
|
|
46
60
|
deleteDoc(id: string, project: string): Promise<boolean>;
|
|
47
61
|
moveDoc(oldId: string, newId: string, project: string): Promise<boolean>;
|
|
48
62
|
getBundles(): ExportBundle[];
|
|
@@ -151,11 +151,22 @@ export class StorageEngine {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
async listDocs(project) {
|
|
154
|
+
return (await this.listDocsDetailed(project)).map((listing) => listing.summary);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Same listing, plus the tracked files parsed from the very same read.
|
|
158
|
+
*
|
|
159
|
+
* Freshness has to know what each doc tracks, and fetching that with `getDoc` per summary read
|
|
160
|
+
* and re-parsed every file of the project a second time. Callers that scan a whole project use
|
|
161
|
+
* this instead; `tracked_files` stays out of `DocSummary` so it is never shipped to the GUI,
|
|
162
|
+
* which only needs the stale/broken flags.
|
|
163
|
+
*/
|
|
164
|
+
async listDocsDetailed(project) {
|
|
154
165
|
const projectDir = this.getProjectDir(project);
|
|
155
166
|
if (!fs.existsSync(projectDir))
|
|
156
167
|
return [];
|
|
157
168
|
const files = this.walkMdFiles(projectDir);
|
|
158
|
-
const
|
|
169
|
+
const listings = [];
|
|
159
170
|
for (const filePath of files) {
|
|
160
171
|
try {
|
|
161
172
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
@@ -164,21 +175,24 @@ export class StorageEngine {
|
|
|
164
175
|
const relPath = path.relative(projectDir, filePath);
|
|
165
176
|
const id = relPath.replace(/\.md$/, "").split(path.sep).join("/");
|
|
166
177
|
const folder = id.includes("/") ? id.split("/").slice(0, -1).join("/") : undefined;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
listings.push({
|
|
179
|
+
summary: {
|
|
180
|
+
id,
|
|
181
|
+
title: data.title ?? "",
|
|
182
|
+
createdAt: stat.birthtime.toISOString(),
|
|
183
|
+
updatedAt: data.updatedAt ?? "",
|
|
184
|
+
tags: (data.tags ?? []),
|
|
185
|
+
folder,
|
|
186
|
+
isStale: false,
|
|
187
|
+
},
|
|
188
|
+
tracked_files: (data.tracked_files ?? []),
|
|
175
189
|
});
|
|
176
190
|
}
|
|
177
191
|
catch (err) {
|
|
178
192
|
logger.warn(`Failed to read doc ${filePath}: ${err}`);
|
|
179
193
|
}
|
|
180
194
|
}
|
|
181
|
-
return
|
|
195
|
+
return listings;
|
|
182
196
|
}
|
|
183
197
|
// resolveDoc() (first project whose tree contained the path) was removed on purpose:
|
|
184
198
|
// it silently sent reads and writes to the wrong project when the same doc path
|
package/dist/src/index.js
CHANGED
|
@@ -17,7 +17,8 @@ import { getDoc } from "./tools/get-doc.js";
|
|
|
17
17
|
import { listDocs } from "./tools/list-docs.js";
|
|
18
18
|
import { updateDoc } from "./tools/update-doc.js";
|
|
19
19
|
import { searchDocs } from "./tools/search-docs.js";
|
|
20
|
-
import { checkFreshness, computeFreshness } from "./tools/check-freshness.js";
|
|
20
|
+
import { checkFreshness, computeFreshness, DOC_CONCURRENCY } from "./tools/check-freshness.js";
|
|
21
|
+
import { mapWithConcurrency } from "./core/concurrency.js";
|
|
21
22
|
import { findAndHashSection } from "./core/section-tracker.js";
|
|
22
23
|
import { getCodeSource } from "./core/code-source.js";
|
|
23
24
|
import { runInCodeOperation } from "./core/code-operation.js";
|
|
@@ -203,16 +204,15 @@ function requireProjectFromIPC(msg) {
|
|
|
203
204
|
}
|
|
204
205
|
/** Loads one project's summaries and flags each entry as stale/broken. */
|
|
205
206
|
async function listDocsWithFreshness(project) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return docs;
|
|
207
|
+
const listings = await storageEngine.listDocsDetailed(project);
|
|
208
|
+
await mapWithConcurrency(listings, DOC_CONCURRENCY, async ({ summary, tracked_files }) => {
|
|
209
|
+
if (tracked_files.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
const report = await computeFreshness(tracked_files, project);
|
|
212
|
+
summary.isStale = report.status === "stale" || report.status === "broken";
|
|
213
|
+
summary.isBroken = report.status === "broken";
|
|
214
|
+
});
|
|
215
|
+
return listings.map((listing) => listing.summary);
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
218
|
* Tells every connected GUI that one project's docs changed. Clients showing
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { TrackedFile, FreshnessReport } from "../../shared/schemas/doc.schema.js";
|
|
2
2
|
import type { StorageEngine } from "../core/storage-engine.js";
|
|
3
|
+
/**
|
|
4
|
+
* Docs checked at once during a whole-project scan. With FILE_CONCURRENCY this caps in-flight
|
|
5
|
+
* source reads at 32 — enough to hide network latency on a 160-doc project, far below anything
|
|
6
|
+
* GitHub objects to.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DOC_CONCURRENCY = 8;
|
|
3
9
|
export declare function computeFreshness(trackedFiles: TrackedFile[], project: string): Promise<FreshnessReport>;
|
|
4
10
|
export declare function formatFreshnessReport(title: string, report: FreshnessReport): string;
|
|
5
11
|
export declare function checkFreshness(storageEngine: StorageEngine, args: unknown): Promise<{
|
|
@@ -1,94 +1,74 @@
|
|
|
1
1
|
import { CheckFreshnessArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
2
2
|
import { findAndHashSection, fileContentMatches } from "../core/section-tracker.js";
|
|
3
3
|
import { getCodeSource } from "../core/code-source.js";
|
|
4
|
+
import { mapWithConcurrency } from "../core/concurrency.js";
|
|
4
5
|
import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
|
|
6
|
+
/** Files of one doc checked at once. Kept modest: docs are also scanned several at a time. */
|
|
7
|
+
const FILE_CONCURRENCY = 4;
|
|
8
|
+
/**
|
|
9
|
+
* Docs checked at once during a whole-project scan. With FILE_CONCURRENCY this caps in-flight
|
|
10
|
+
* source reads at 32 — enough to hide network latency on a 160-doc project, far below anything
|
|
11
|
+
* GitHub objects to.
|
|
12
|
+
*/
|
|
13
|
+
export const DOC_CONCURRENCY = 8;
|
|
14
|
+
/** Compares one tracked file with the source, never throwing — the failure is part of the verdict. */
|
|
15
|
+
async function checkTrackedFile(tracked, source) {
|
|
16
|
+
try {
|
|
17
|
+
// When the source can identify the content without transferring it and that identity is
|
|
18
|
+
// unchanged, the file is provably untouched — sections included — so nothing is read.
|
|
19
|
+
if (tracked.sourceBlobId && (await source.contentId(tracked.path)) === tracked.sourceBlobId) {
|
|
20
|
+
return { kind: "unchanged" };
|
|
21
|
+
}
|
|
22
|
+
const file = await source.read(tracked.path);
|
|
23
|
+
// A path that does not resolve at the source counts as absent, so a doc whose files are all
|
|
24
|
+
// gone ends up "unverifiable" rather than crying wolf with "stale".
|
|
25
|
+
if (!file)
|
|
26
|
+
return { kind: "missing" };
|
|
27
|
+
if (tracked.sections && tracked.sections.length > 0) {
|
|
28
|
+
for (const section of tracked.sections) {
|
|
29
|
+
const result = findAndHashSection(file.content, section);
|
|
30
|
+
if (!result.found)
|
|
31
|
+
return { kind: "anchors_lost" };
|
|
32
|
+
if (!result.matchesStored)
|
|
33
|
+
return { kind: "modified" };
|
|
34
|
+
}
|
|
35
|
+
return { kind: "unchanged" };
|
|
36
|
+
}
|
|
37
|
+
if (tracked.contentHash) {
|
|
38
|
+
return fileContentMatches(file.content, tracked.contentHash) ? { kind: "unchanged" } : { kind: "modified" };
|
|
39
|
+
}
|
|
40
|
+
// Hash comparison is the only freshness system. An entry with neither sections nor
|
|
41
|
+
// contentHash predates it and cannot be checked — it is reported as broken (never checked by
|
|
42
|
+
// any other means) so readers can still expose the doc and its tracked list for re-tracking.
|
|
43
|
+
return { kind: "broken" };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// The source itself is unreachable (network, auth, rate limit). Nothing can be compared, and
|
|
47
|
+
// reporting what we did not manage to read as "modified" would be crying wolf.
|
|
48
|
+
return { kind: "source_error", message: err instanceof Error ? err.message : String(err) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
5
51
|
export async function computeFreshness(trackedFiles, project) {
|
|
6
52
|
const source = getCodeSource(project);
|
|
7
53
|
const empty = { modified: [], missing: [], unchanged: [], anchors_lost: [], broken: [], source: source.label };
|
|
8
54
|
if (!trackedFiles || trackedFiles.length === 0) {
|
|
9
55
|
return { status: "no_tracking", ...empty, total: 0 };
|
|
10
56
|
}
|
|
57
|
+
const verdicts = await mapWithConcurrency(trackedFiles, FILE_CONCURRENCY, (tracked) => checkTrackedFile(tracked, source));
|
|
58
|
+
const failed = verdicts.find((verdict) => verdict.kind === "source_error");
|
|
59
|
+
if (failed) {
|
|
60
|
+
return { status: "unverifiable", ...empty, total: trackedFiles.length, sourceError: failed.message };
|
|
61
|
+
}
|
|
11
62
|
const modified = [];
|
|
12
63
|
const missing = [];
|
|
13
64
|
const unchanged = [];
|
|
14
65
|
const anchors_lost = [];
|
|
15
66
|
const broken = [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (tracked.sourceBlobId && (await source.contentId(tracked.path)) === tracked.sourceBlobId) {
|
|
22
|
-
unchanged.push(tracked.path);
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
file = await source.read(tracked.path);
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
// The source itself is unreachable (network, auth, rate limit). Nothing can be compared, and
|
|
29
|
-
// reporting what we did not manage to read as "modified" would be crying wolf.
|
|
30
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
-
return {
|
|
32
|
-
status: "unverifiable",
|
|
33
|
-
modified: [],
|
|
34
|
-
missing: [],
|
|
35
|
-
unchanged: [],
|
|
36
|
-
anchors_lost: [],
|
|
37
|
-
broken: [],
|
|
38
|
-
total: trackedFiles.length,
|
|
39
|
-
source: source.label,
|
|
40
|
-
sourceError: message,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
// A path that does not resolve at the source counts as absent, so we end up "unverifiable"
|
|
44
|
-
// rather than crying wolf with "stale".
|
|
45
|
-
if (!file) {
|
|
46
|
-
missing.push(tracked.path);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (tracked.sections && tracked.sections.length > 0) {
|
|
50
|
-
let sectionStale = false;
|
|
51
|
-
let anchorLost = false;
|
|
52
|
-
for (const section of tracked.sections) {
|
|
53
|
-
const result = findAndHashSection(file.content, section);
|
|
54
|
-
if (!result.found) {
|
|
55
|
-
sectionStale = true;
|
|
56
|
-
anchorLost = true;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
else if (!result.matchesStored) {
|
|
60
|
-
sectionStale = true;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (sectionStale) {
|
|
65
|
-
if (anchorLost) {
|
|
66
|
-
anchors_lost.push(tracked.path);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
modified.push(tracked.path);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
unchanged.push(tracked.path);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else if (tracked.contentHash) {
|
|
77
|
-
if (fileContentMatches(file.content, tracked.contentHash)) {
|
|
78
|
-
unchanged.push(tracked.path);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
modified.push(tracked.path);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
// Hash comparison is the only freshness system. An entry with neither
|
|
86
|
-
// sections nor contentHash predates it and cannot be checked — it is
|
|
87
|
-
// reported as broken (never checked by any other means) so readers can
|
|
88
|
-
// still expose the doc and its tracked list to the agent for re-tracking.
|
|
89
|
-
broken.push(tracked.path);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
67
|
+
const buckets = { modified, missing, unchanged, anchors_lost, broken };
|
|
68
|
+
verdicts.forEach((verdict, i) => {
|
|
69
|
+
if (verdict.kind !== "source_error")
|
|
70
|
+
buckets[verdict.kind].push(trackedFiles[i].path);
|
|
71
|
+
});
|
|
92
72
|
// When every tracked file is absent, the source code simply isn't present at the source
|
|
93
73
|
// (e.g. docs cloned without the codebase, or a branch where those files don't exist) — we
|
|
94
74
|
// can't verify freshness, so report "unverifiable" rather than crying wolf with "stale".
|
|
@@ -173,9 +153,9 @@ function renderUntrackedList(titles, heading) {
|
|
|
173
153
|
return `\n\n${heading}\n${shown.join("\n")}${suffix}`;
|
|
174
154
|
}
|
|
175
155
|
async function scanProjectFreshness(storageEngine, project) {
|
|
176
|
-
const
|
|
156
|
+
const listings = await storageEngine.listDocsDetailed(project);
|
|
177
157
|
const scan = {
|
|
178
|
-
docCount:
|
|
158
|
+
docCount: listings.length,
|
|
179
159
|
stale: [],
|
|
180
160
|
noTracking: [],
|
|
181
161
|
broken: [],
|
|
@@ -183,11 +163,11 @@ async function scanProjectFreshness(storageEngine, project) {
|
|
|
183
163
|
unverifiable: 0,
|
|
184
164
|
source: getCodeSource(project).label,
|
|
185
165
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
166
|
+
const checked = await mapWithConcurrency(listings, DOC_CONCURRENCY, async ({ summary, tracked_files }) => ({
|
|
167
|
+
doc: summary,
|
|
168
|
+
report: await computeFreshness(tracked_files, project),
|
|
169
|
+
}));
|
|
170
|
+
for (const { doc, report } of checked) {
|
|
191
171
|
if (report.sourceError)
|
|
192
172
|
scan.sourceError = report.sourceError;
|
|
193
173
|
if (report.status === "broken") {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { computeFreshness } from "./check-freshness.js";
|
|
1
|
+
import { computeFreshness, DOC_CONCURRENCY } from "./check-freshness.js";
|
|
2
|
+
import { mapWithConcurrency } from "../core/concurrency.js";
|
|
3
|
+
/**
|
|
4
|
+
* Fills in each summary's staleness flags, checking several docs at once — a listing is a
|
|
5
|
+
* whole-project scan, and doing it one doc at a time is what made large projects time out.
|
|
6
|
+
*/
|
|
7
|
+
async function flagStaleness(listings, project) {
|
|
8
|
+
await mapWithConcurrency(listings, DOC_CONCURRENCY, async ({ summary, tracked_files }) => {
|
|
9
|
+
if (tracked_files.length === 0)
|
|
10
|
+
return;
|
|
11
|
+
const report = await computeFreshness(tracked_files, project);
|
|
12
|
+
summary.isStale = report.status === "stale";
|
|
13
|
+
summary.isBroken = report.status === "broken";
|
|
14
|
+
});
|
|
15
|
+
return listings.map((listing) => listing.summary);
|
|
16
|
+
}
|
|
2
17
|
function renderProjectDocs(summaries, totalLabel) {
|
|
3
18
|
if (summaries.length === 0)
|
|
4
19
|
return " (no entries)";
|
|
@@ -37,15 +52,7 @@ export async function listDocs(storageEngine, args) {
|
|
|
37
52
|
const isMultiProject = projectsToList.length > 1;
|
|
38
53
|
if (!isMultiProject) {
|
|
39
54
|
const proj = projectsToList[0];
|
|
40
|
-
const summaries = await storageEngine.
|
|
41
|
-
for (const summary of summaries) {
|
|
42
|
-
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
43
|
-
if (doc && doc.tracked_files.length > 0) {
|
|
44
|
-
const report = await computeFreshness(doc.tracked_files, proj);
|
|
45
|
-
summary.isStale = report.status === "stale";
|
|
46
|
-
summary.isBroken = report.status === "broken";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
55
|
+
const summaries = await flagStaleness(await storageEngine.listDocsDetailed(proj), proj);
|
|
49
56
|
if (summaries.length === 0) {
|
|
50
57
|
return { content: [{ type: "text", text: `No documentation entries found in project "${proj}".` }] };
|
|
51
58
|
}
|
|
@@ -56,15 +63,7 @@ export async function listDocs(storageEngine, args) {
|
|
|
56
63
|
const lines = [];
|
|
57
64
|
let grandTotal = 0;
|
|
58
65
|
for (const proj of projectsToList) {
|
|
59
|
-
const summaries = await storageEngine.
|
|
60
|
-
for (const summary of summaries) {
|
|
61
|
-
const doc = await storageEngine.getDoc(summary.id, proj);
|
|
62
|
-
if (doc && doc.tracked_files.length > 0) {
|
|
63
|
-
const report = await computeFreshness(doc.tracked_files, proj);
|
|
64
|
-
summary.isStale = report.status === "stale";
|
|
65
|
-
summary.isBroken = report.status === "broken";
|
|
66
|
-
}
|
|
67
|
-
}
|
|
66
|
+
const summaries = await flagStaleness(await storageEngine.listDocsDetailed(proj), proj);
|
|
68
67
|
grandTotal += summaries.length;
|
|
69
68
|
lines.push(`\n🗂 ${proj} (${summaries.length})`);
|
|
70
69
|
lines.push(renderProjectDocs(summaries, ""));
|
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.80";
|
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.80';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a0 as ln,a1 as an,a2 as y,a3 as tn,a4 as H,a5 as q,a6 as _,a7 as un,a8 as rn,a9 as L,aa as o,ab as B,ac as sn,ad as on,ae as fn}from"./index-
|
|
1
|
+
import{a0 as ln,a1 as an,a2 as y,a3 as tn,a4 as H,a5 as q,a6 as _,a7 as un,a8 as rn,a9 as L,aa as o,ab as B,ac as sn,ad as on,ae as fn}from"./index-T7ZLHNgs.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,C,a){var O=I-l,i=D-h,n=C-v,d=a-A,u=d*O-n*i;if(!(u*u<y))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function W(l,h,I,D,v,A,C){var a=l-I,O=h-D,i=(C?A:-A)/L(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,F=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,R=m*m+g*g,T=v-A,P=u*c-f*s,S=(g<0?-1:1)*L(fn(0,T*T*R-P*P)),j=(P*g-m*S)/R,z=(-P*m-g*S)/R,w=(P*g+m*S)/R,p=(-P*m+g*S)/R,x=j-F,e=z-t,r=w-F,G=p-t;return x*x+e*e>r*r+G*G&&(j=w,z=p),{cx:j,cy:z,x01:-n,y01:-d,x11:j*(v/T-1),y11:z*(v/T-1)}}function hn(){var l=cn,h=yn,I=B(0),D=null,v=gn,A=dn,C=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-an,c=A.apply(this,arguments)-an,F=un(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(F>tn-y)a.moveTo(s*H(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*H(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,R=f,T=c,P=F,S=F,j=C.apply(this,arguments)/2,z=j>y&&(D?+D.apply(this,arguments):L(u*u+s*s)),w=_(un(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if(z>y){var G=sn(z/u*q(j)),M=sn(z/s*q(j));(P-=G*2)>y?(G*=t?1:-1,R+=G,T-=G):(P=0,R=T=(f+c)/2),(S-=M*2)>y?(M*=t?1:-1,m+=M,g-=M):(S=0,m=g=(f+c)/2)}var J=s*H(m),K=s*q(m),N=u*H(T),Q=u*q(T);if(w>y){var U=s*H(g),V=s*q(g),X=u*H(R),Y=u*q(R),E;if(F<rn)if(E=pn(J,K,X,Y,U,V,N,Q)){var Z=J-E[0],$=K-E[1],b=U-E[0],k=V-E[1],nn=1/q(on((Z*b+$*k)/(L(Z*Z+$*$)*L(b*b+k*k)))/2),en=L(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>y?x>y?(e=W(X,Y,J,K,s,x,t),r=W(U,V,N,Q,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(J,K),a.arc(0,0,s,m,g,!t)):a.moveTo(J,K),!(u>y)||!(P>y)?a.lineTo(N,Q):p>y?(e=W(N,Q,U,V,u,-p,t),r=W(J,K,X,Y,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-rn/2;return[H(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:B(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:B(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:B(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:B(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:B(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:B(+n),i):A},i.padAngle=function(n){return arguments.length?(C=typeof n=="function"?n:B(+n),i):C},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|