@imenam/mcp-documentor 1.0.87 → 1.0.88

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 (79) hide show
  1. package/README.md +7 -5
  2. package/dist/shared/read-only.js +1 -1
  3. package/dist/shared/schemas/doc.schema.d.ts +1 -0
  4. package/dist/shared/schemas/doc.schema.js +1 -0
  5. package/dist/src/core/code-source.d.ts +32 -0
  6. package/dist/src/core/code-source.js +23 -4
  7. package/dist/src/core/git-sync.d.ts +5 -0
  8. package/dist/src/core/git-sync.js +29 -2
  9. package/dist/src/core/github-code-api.d.ts +3 -8
  10. package/dist/src/core/github-code-api.js +67 -36
  11. package/dist/src/core/github-log.d.ts +38 -0
  12. package/dist/src/core/github-log.js +64 -0
  13. package/dist/src/core/log-reader.d.ts +47 -0
  14. package/dist/src/core/log-reader.js +106 -0
  15. package/dist/src/core/storage-engine.d.ts +21 -6
  16. package/dist/src/core/storage-engine.js +80 -24
  17. package/dist/src/gui-worker.js +58 -1
  18. package/dist/src/index.js +75 -10
  19. package/dist/src/tools/code-sources.d.ts +9 -0
  20. package/dist/src/tools/code-sources.js +18 -16
  21. package/dist/src/tools/create-project.d.ts +19 -0
  22. package/dist/src/tools/create-project.js +42 -0
  23. package/dist/src/tools/delete-project.d.ts +16 -0
  24. package/dist/src/tools/delete-project.js +25 -0
  25. package/dist/src/tools/git-diff-doc.js +123 -98
  26. package/dist/src/tools/patch-doc.js +14 -3
  27. package/gui/dist/assets/{arc-DBgCQU2O.js → arc-s4GL8QZ_.js} +1 -1
  28. package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BIPBAzZr.js → architectureDiagram-3BPJPVTR-CfDERg0r.js} +1 -1
  29. package/gui/dist/assets/{blockDiagram-GPEHLZMM-BE9WwuyF.js → blockDiagram-GPEHLZMM-CjDoEG3-.js} +1 -1
  30. package/gui/dist/assets/{c4Diagram-AAUBKEIU-V1_4ktIt.js → c4Diagram-AAUBKEIU-9o7f2UhK.js} +1 -1
  31. package/gui/dist/assets/channel-BsKHRH5P.js +1 -0
  32. package/gui/dist/assets/{chunk-2J33WTMH-DskG-Kmf.js → chunk-2J33WTMH-B37ouRYO.js} +1 -1
  33. package/gui/dist/assets/{chunk-4BX2VUAB-DaPhidEI.js → chunk-4BX2VUAB-Dcq44Skv.js} +1 -1
  34. package/gui/dist/assets/{chunk-55IACEB6-C9r6WDFM.js → chunk-55IACEB6-CLyM-RQf.js} +1 -1
  35. package/gui/dist/assets/{chunk-727SXJPM-BA-aC8US.js → chunk-727SXJPM-BjzEo2iz.js} +1 -1
  36. package/gui/dist/assets/{chunk-AQP2D5EJ-ByBp3whV.js → chunk-AQP2D5EJ-Co03OHPi.js} +1 -1
  37. package/gui/dist/assets/{chunk-FMBD7UC4-CzqUByx7.js → chunk-FMBD7UC4-ClbF2rAp.js} +1 -1
  38. package/gui/dist/assets/{chunk-ND2GUHAM-C0_B2ryQ.js → chunk-ND2GUHAM-CN5cTCWu.js} +1 -1
  39. package/gui/dist/assets/{chunk-QZHKN3VN-B3eEwjDh.js → chunk-QZHKN3VN-Bg4-kEPK.js} +1 -1
  40. package/gui/dist/assets/classDiagram-4FO5ZUOK-DphzXyc8.js +1 -0
  41. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-DphzXyc8.js +1 -0
  42. package/gui/dist/assets/{cose-bilkent-S5V4N54A-D_i0I2pU.js → cose-bilkent-S5V4N54A-6Dkp59bE.js} +1 -1
  43. package/gui/dist/assets/{dagre-BM42HDAG-DtEVPK_z.js → dagre-BM42HDAG-2dD-ID2T.js} +1 -1
  44. package/gui/dist/assets/{diagram-2AECGRRQ-BftnXa5w.js → diagram-2AECGRRQ-DBE2f2Lb.js} +1 -1
  45. package/gui/dist/assets/{diagram-5GNKFQAL-Dh9-kl_u.js → diagram-5GNKFQAL-CkKPHiH9.js} +1 -1
  46. package/gui/dist/assets/{diagram-KO2AKTUF-CvB0BWCa.js → diagram-KO2AKTUF-uWtGAF2N.js} +1 -1
  47. package/gui/dist/assets/{diagram-LMA3HP47-CDy-J1uy.js → diagram-LMA3HP47-DyQaWg_0.js} +1 -1
  48. package/gui/dist/assets/{diagram-OG6HWLK6-iY9FwkIF.js → diagram-OG6HWLK6-H_DfuB5t.js} +1 -1
  49. package/gui/dist/assets/{erDiagram-TEJ5UH35-BT2_IX8D.js → erDiagram-TEJ5UH35-Cj9K5EXA.js} +1 -1
  50. package/gui/dist/assets/{flowDiagram-I6XJVG4X-DaixWyMx.js → flowDiagram-I6XJVG4X-BYF24Jvt.js} +1 -1
  51. package/gui/dist/assets/{ganttDiagram-6RSMTGT7-DSo3cQPH.js → ganttDiagram-6RSMTGT7-D93LjmhI.js} +1 -1
  52. package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-BfFT7Lds.js → gitGraphDiagram-PVQCEYII-EVTcusDb.js} +1 -1
  53. package/gui/dist/assets/index-CtPuh-0k.css +1 -0
  54. package/gui/dist/assets/{index-DPGmAc6z.js → index-DvVxuv5N.js} +154 -152
  55. package/gui/dist/assets/{infoDiagram-5YYISTIA-BxbxNxe2.js → infoDiagram-5YYISTIA-DeGbaexj.js} +1 -1
  56. package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DpopzqZr.js → ishikawaDiagram-YF4QCWOH-DNdk7sPJ.js} +1 -1
  57. package/gui/dist/assets/{journeyDiagram-JHISSGLW-DrOlFzIk.js → journeyDiagram-JHISSGLW-yLLfyn3W.js} +1 -1
  58. package/gui/dist/assets/{kanban-definition-UN3LZRKU-vH81lYXy.js → kanban-definition-UN3LZRKU-D8yvVgr8.js} +1 -1
  59. package/gui/dist/assets/{linear-BipFpFah.js → linear-j3lplktA.js} +1 -1
  60. package/gui/dist/assets/{mindmap-definition-RKZ34NQL-DsTxCLgj.js → mindmap-definition-RKZ34NQL-t9adVeMH.js} +1 -1
  61. package/gui/dist/assets/{pieDiagram-4H26LBE5-Bbq2ZWQU.js → pieDiagram-4H26LBE5-6Yrvo8h8.js} +1 -1
  62. package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BdTSaEo4.js → quadrantDiagram-W4KKPZXB-C_3Su6rX.js} +1 -1
  63. package/gui/dist/assets/{requirementDiagram-4Y6WPE33-BndI8YZy.js → requirementDiagram-4Y6WPE33-B1qYSC11.js} +1 -1
  64. package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-ofBkVITf.js → sankeyDiagram-5OEKKPKP-D3Ervn-u.js} +1 -1
  65. package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-Cjj2UrN9.js → sequenceDiagram-3UESZ5HK-CKgTeWBS.js} +1 -1
  66. package/gui/dist/assets/{stateDiagram-AJRCARHV-CzPrXUuz.js → stateDiagram-AJRCARHV-D_PjEdFH.js} +1 -1
  67. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-KPbsissC.js +1 -0
  68. package/gui/dist/assets/{timeline-definition-PNZ67QCA-Cz4X-IOO.js → timeline-definition-PNZ67QCA-HeWRiNHu.js} +1 -1
  69. package/gui/dist/assets/{vennDiagram-CIIHVFJN-BwIgXJcZ.js → vennDiagram-CIIHVFJN-CREk7vzm.js} +1 -1
  70. package/gui/dist/assets/{wardley-L42UT6IY-keQS0aNZ.js → wardley-L42UT6IY-BWtRQ7ZH.js} +1 -1
  71. package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-DhykSgbD.js → wardleyDiagram-YWT4CUSO-BvlqWWwp.js} +1 -1
  72. package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DFTqc4cj.js → xychartDiagram-2RQKCTM6-BCjv6pwc.js} +1 -1
  73. package/gui/dist/index.html +2 -2
  74. package/package.json +2 -1
  75. package/gui/dist/assets/channel-B_cci0fU.js +0 -1
  76. package/gui/dist/assets/classDiagram-4FO5ZUOK-BqlcJ5BP.js +0 -1
  77. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-BqlcJ5BP.js +0 -1
  78. package/gui/dist/assets/index-CxCC1gxu.css +0 -1
  79. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DAq4ObN9.js +0 -1
@@ -0,0 +1,19 @@
1
+ import type { StorageEngine } from "../core/storage-engine.js";
2
+ import type { LocalState } from "../core/local-state.js";
3
+ /**
4
+ * Creates an empty project, with its code source when one is given. The source is checked before
5
+ * anything is written, so an unknown repository or branch creates nothing.
6
+ */
7
+ export declare function createProject(storageEngine: StorageEngine, localState: LocalState, args: unknown): Promise<{
8
+ isError: boolean;
9
+ content: {
10
+ type: string;
11
+ text: string;
12
+ }[];
13
+ } | {
14
+ content: {
15
+ type: string;
16
+ text: string;
17
+ }[];
18
+ isError?: undefined;
19
+ }>;
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import { describeCodeSourceConfig, resolveCodeSourceInput, saveCodeSource } from "./code-sources.js";
3
+ const CreateProjectArgsSchema = z.object({
4
+ project: z
5
+ .string()
6
+ .trim()
7
+ .min(1)
8
+ .refine((name) => name !== "." && name !== ".." && !/[\\/:]/.test(name), {
9
+ message: 'Project names cannot be "." or "..", nor contain "/", "\\" or ":".',
10
+ })
11
+ .describe("Name of the project to create."),
12
+ code_source: z
13
+ .object({
14
+ kind: z.enum(["local", "github"]),
15
+ repo: z.string().optional(),
16
+ branch: z.string().optional(),
17
+ })
18
+ .optional()
19
+ .describe("Where the project's code is read from, as with set_code_source. Omitted: the local working copy."),
20
+ });
21
+ /**
22
+ * Creates an empty project, with its code source when one is given. The source is checked before
23
+ * anything is written, so an unknown repository or branch creates nothing.
24
+ */
25
+ export async function createProject(storageEngine, localState, args) {
26
+ try {
27
+ const { project, code_source } = CreateProjectArgsSchema.parse(args);
28
+ if (storageEngine.getConfig().projects.includes(project)) {
29
+ return { isError: true, content: [{ type: "text", text: `Project "${project}" already exists.` }] };
30
+ }
31
+ const config = code_source ? await resolveCodeSourceInput(code_source) : undefined;
32
+ storageEngine.ensureProject(project);
33
+ if (config)
34
+ saveCodeSource(storageEngine, localState, project, config);
35
+ const source = config ? ` It reads its code from ${describeCodeSourceConfig(config)}.` : "";
36
+ return { content: [{ type: "text", text: `Project "${project}" created.${source}` }] };
37
+ }
38
+ catch (err) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ return { isError: true, content: [{ type: "text", text: `Error creating project: ${message}` }] };
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ import type { StorageEngine } from "../core/storage-engine.js";
2
+ import type { LocalState } from "../core/local-state.js";
3
+ /** Deletes an empty project, along with this machine's code root for it. */
4
+ export declare function deleteProject(storageEngine: StorageEngine, localState: LocalState, args: unknown): Promise<{
5
+ content: {
6
+ type: string;
7
+ text: string;
8
+ }[];
9
+ isError?: undefined;
10
+ } | {
11
+ isError: boolean;
12
+ content: {
13
+ type: string;
14
+ text: string;
15
+ }[];
16
+ }>;
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { invalidateCodeSources } from "../core/code-source.js";
3
+ import { assertProject, scopeErrorResult } from "../core/project-scope.js";
4
+ const DeleteProjectArgsSchema = z.object({
5
+ project: z.string().min(1).describe("Project to delete. It must be empty: no doc, no image, no other file."),
6
+ });
7
+ /** Deletes an empty project, along with this machine's code root for it. */
8
+ export async function deleteProject(storageEngine, localState, args) {
9
+ try {
10
+ const { project } = DeleteProjectArgsSchema.parse(args);
11
+ const projectError = assertProject(storageEngine, project);
12
+ if (projectError)
13
+ return scopeErrorResult(projectError);
14
+ storageEngine.deleteProject(project);
15
+ localState.setProjectCodeRoot(project, undefined);
16
+ invalidateCodeSources(project);
17
+ return {
18
+ content: [{ type: "text", text: `Project "${project}" deleted.` }],
19
+ };
20
+ }
21
+ catch (err) {
22
+ const message = err instanceof Error ? err.message : String(err);
23
+ return { isError: true, content: [{ type: "text", text: `Error deleting project: ${message}` }] };
24
+ }
25
+ }
@@ -1,117 +1,144 @@
1
- import { execSync } from "child_process";
1
+ import { execFileSync } from "child_process";
2
2
  import path from "path";
3
+ import { createTwoFilesPatch } from "diff";
3
4
  import { GitDiffDocArgsSchema } from "../../shared/schemas/doc.schema.js";
4
5
  import { computeFreshness } from "./check-freshness.js";
5
6
  import { toAbsolutePath } from "../core/path-resolver.js";
6
- import { getCodeSource, getGitHubApi, toStoredPathForMatching } from "../core/code-source.js";
7
+ import { getCodeSource, getGitHubSource, toStoredPathForMatching } from "../core/code-source.js";
8
+ import { bytesToFileContent, fileContentMatches, findAndHashSection } from "../core/section-tracker.js";
9
+ import { mapWithConcurrency } from "../core/concurrency.js";
7
10
  import { assertProject, scopeErrorResult } from "../core/project-scope.js";
8
- /** Diff of one tracked file against the local working copy, via the git CLI. */
9
- function getLocalGitDiff(tracked, project) {
10
- const filePath = toAbsolutePath(tracked.path, project);
11
- if (!filePath) {
12
- return ` [INFO] Cannot resolve "${tracked.path}" on this machine (no code root configured) — source absent.`;
11
+ /** Tracked files of one doc diffed at once. */
12
+ const FILE_CONCURRENCY = 4;
13
+ const errorMessage = (err) => (err instanceof Error ? err.message : String(err));
14
+ const short = (sha) => sha.slice(0, 7);
15
+ /**
16
+ * Snapshot content on GitHub, by decreasing reliability: the recorded blob (independent of any
17
+ * commit, so it survives a rebase), the file at the recorded commit, or the file at the newest
18
+ * commit on the branch at or before the snapshot date.
19
+ */
20
+ async function githubSnapshotBase(source, tracked) {
21
+ const repo = source.api.label;
22
+ if (tracked.sourceBlobId) {
23
+ try {
24
+ return { content: await source.readBlob(tracked.sourceBlobId, tracked.path), origin: `snapshot blob ${short(tracked.sourceBlobId)}`, inferred: false };
25
+ }
26
+ catch (err) {
27
+ return { error: `The snapshot content (blob ${short(tracked.sourceBlobId)}) cannot be read on ${repo}: ${errorMessage(err)}` };
28
+ }
13
29
  }
14
- const fileDir = path.dirname(filePath);
15
- let repoRoot;
30
+ let commit = tracked.sourceRevision;
31
+ const inferred = !commit;
32
+ if (!commit) {
33
+ try {
34
+ commit = await source.api.resolveCommitAtDate(tracked.lastModifiedAt);
35
+ }
36
+ catch (err) {
37
+ return { error: `No source commit recorded, and the commit at the snapshot date (${tracked.lastModifiedAt}) could not be resolved on ${repo}: ${errorMessage(err)}` };
38
+ }
39
+ if (!commit) {
40
+ return { error: `No source commit recorded, and ${repo} has no commit at or before the snapshot date (${tracked.lastModifiedAt}).` };
41
+ }
42
+ }
43
+ const origin = inferred ? `commit ${short(commit)} inferred from the snapshot date` : `snapshot commit ${short(commit)}`;
16
44
  try {
17
- repoRoot = execSync(`git -C "${fileDir}" rev-parse --show-toplevel`, { encoding: "utf-8" }).trim();
45
+ const content = await source.readAt(tracked.path, commit);
46
+ if (!content)
47
+ return { error: `The file does not exist at ${origin} on ${repo}.` };
48
+ return { content, origin, inferred };
18
49
  }
19
- catch {
20
- return ` [ERROR] Not a git repository: ${filePath}`;
50
+ catch (err) {
51
+ return { error: `The ${origin} cannot be read on ${repo}: ${errorMessage(err)}` };
21
52
  }
22
- const relativeFilePath = path.relative(repoRoot, filePath).replace(/\\/g, "/");
23
- let commitHash;
53
+ }
54
+ function git(repoRoot, args) {
55
+ return execFileSync("git", ["-C", repoRoot, ...args], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
56
+ }
57
+ /** Snapshot content in the local clone, with the same order of preference as on GitHub. */
58
+ function localSnapshotBase(tracked, project) {
59
+ const filePath = toAbsolutePath(tracked.path, project);
60
+ if (!filePath)
61
+ return { error: `Cannot resolve "${tracked.path}" on this machine (no code root configured).` };
62
+ let repoRoot;
24
63
  try {
25
- commitHash = execSync(`git -C "${repoRoot}" log --before="${tracked.lastModifiedAt}" -1 --format=%H -- "${relativeFilePath}"`, { encoding: "utf-8" }).trim();
64
+ repoRoot = git(path.dirname(filePath), ["rev-parse", "--show-toplevel"]).toString("utf-8").trim();
26
65
  }
27
66
  catch {
28
- return ` [ERROR] Could not retrieve git log for: ${filePath}`;
67
+ return { error: `Not a git repository: ${filePath}` };
29
68
  }
30
- if (!commitHash) {
31
- return ` [INFO] No commit found before snapshot date (${tracked.lastModifiedAt}) for: ${filePath}`;
69
+ const relativeFilePath = path.relative(repoRoot, filePath).replace(/\\/g, "/");
70
+ if (tracked.sourceBlobId) {
71
+ try {
72
+ return { content: bytesToFileContent(git(repoRoot, ["cat-file", "blob", tracked.sourceBlobId])), origin: `snapshot blob ${short(tracked.sourceBlobId)}`, inferred: false };
73
+ }
74
+ catch {
75
+ return { error: `The snapshot content (blob ${short(tracked.sourceBlobId)}) is not in the local repository ${repoRoot}.` };
76
+ }
32
77
  }
33
- let diff;
78
+ let commit = tracked.sourceRevision;
79
+ const inferred = !commit;
80
+ if (!commit) {
81
+ try {
82
+ commit = git(repoRoot, ["log", `--before=${tracked.lastModifiedAt}`, "-1", "--format=%H", "--", relativeFilePath]).toString("utf-8").trim();
83
+ }
84
+ catch {
85
+ return { error: `Could not read the git log of ${filePath}.` };
86
+ }
87
+ if (!commit)
88
+ return { error: `No commit touches ${relativeFilePath} at or before the snapshot date (${tracked.lastModifiedAt}).` };
89
+ }
90
+ const origin = inferred ? `commit ${short(commit)} inferred from the snapshot date` : `snapshot commit ${short(commit)}`;
34
91
  try {
35
- diff = execSync(`git -C "${repoRoot}" diff ${commitHash} -- "${relativeFilePath}"`, { encoding: "utf-8" }).trim();
92
+ return { content: bytesToFileContent(git(repoRoot, ["show", `${commit}:${relativeFilePath}`])), origin, inferred };
36
93
  }
37
94
  catch {
38
- return ` [ERROR] Could not compute diff for: ${filePath}`;
39
- }
40
- if (!diff) {
41
- return ` [INFO] No changes since snapshot (commit ${commitHash.slice(0, 7)}) for: ${filePath}`;
95
+ return { error: `The file does not exist at ${origin} in ${repoRoot}, or that commit is not in the local repository.` };
42
96
  }
43
- return diff;
44
97
  }
45
98
  /**
46
- * Diffs tracked files against the branch they are tracked on, using the commit each snapshot
47
- * was taken at as the base.
48
- *
49
- * A snapshot with no recorded commit — taken before branch tracking, or from a local working copy —
50
- * is anchored on the newest commit at or before its date instead, so it diffs like any other rather
51
- * than demanding a refresh first.
52
- *
53
- * Files are grouped by that base commit so one comparison covers every file snapshotted at the
54
- * same revision — which, for a doc refreshed in one go, means a single API call.
99
+ * Whether a content is the one the snapshot recorded, judged by its stored hashes. Undefined when
100
+ * the entry holds no hash to judge by.
55
101
  */
56
- async function getGitHubDiffs(api, trackedFiles) {
57
- const diffs = new Map();
58
- const byBase = new Map();
59
- const inferredBases = new Set();
60
- for (const tracked of trackedFiles) {
61
- let base = tracked.sourceRevision;
62
- if (!base) {
63
- try {
64
- base = await api.resolveCommitAtDate(tracked.lastModifiedAt);
65
- }
66
- catch (err) {
67
- const message = err instanceof Error ? err.message : String(err);
68
- diffs.set(tracked.path, ` [ERROR] No source commit recorded, and the commit at the snapshot date (${tracked.lastModifiedAt}) could not be resolved on ${api.label}: ${message}`);
69
- continue;
70
- }
71
- if (!base) {
72
- diffs.set(tracked.path, ` [INFO] No source commit recorded, and ${api.label} has no commit at or before the snapshot date (${tracked.lastModifiedAt}) — nothing to diff against.\n` +
73
- ` → Call refresh_file_snapshot to re-anchor this file on ${api.label}.`);
74
- continue;
75
- }
76
- inferredBases.add(base);
77
- }
78
- const group = byBase.get(base);
79
- if (group)
80
- group.push(tracked);
81
- else
82
- byBase.set(base, [tracked]);
83
- }
84
- if (byBase.size === 0)
85
- return diffs;
86
- const { commitSha: head } = await api.resolveRef();
87
- for (const [base, files] of byBase) {
88
- // An inferred base is the branch state at the snapshot date, not a commit the snapshot was
89
- // provably read from — worth saying, since the diff can then include changes already documented.
90
- const note = inferredBases.has(base)
91
- ? ` [INFO] Base commit ${base.slice(0, 7)} inferred from the snapshot date (no source commit recorded).\n\n`
92
- : "";
93
- if (base === head) {
94
- for (const file of files) {
95
- diffs.set(file.path, `${note} [INFO] ${api.label} is still at the snapshot commit ${base.slice(0, 7)} — no changes.`);
96
- }
97
- continue;
98
- }
99
- let patches;
100
- try {
101
- patches = await api.comparePatches(base, head);
102
- }
103
- catch (err) {
104
- const message = err instanceof Error ? err.message : String(err);
105
- for (const file of files)
106
- diffs.set(file.path, `${note} [ERROR] Could not compare ${base.slice(0, 7)}...${head.slice(0, 7)}: ${message}`);
107
- continue;
108
- }
109
- for (const file of files) {
110
- const patch = patches.get(file.path);
111
- diffs.set(file.path, note + (patch ?? ` [INFO] No changes to this file between ${base.slice(0, 7)} and ${head.slice(0, 7)}.`));
112
- }
102
+ function matchesSnapshot(content, tracked) {
103
+ if (tracked.sections && tracked.sections.length > 0) {
104
+ return tracked.sections.every((section) => {
105
+ const result = findAndHashSection(content, section);
106
+ return result.found && result.matchesStored;
107
+ });
108
+ }
109
+ if (tracked.contentHash)
110
+ return fileContentMatches(content, tracked.contentHash);
111
+ return undefined;
112
+ }
113
+ /** The diff section of one tracked file: snapshot content against the current one at the source. */
114
+ async function diffTrackedFile(tracked, project, source) {
115
+ const github = getGitHubSource(project);
116
+ const base = github ? await githubSnapshotBase(github, tracked) : localSnapshotBase(tracked, project);
117
+ const impossible = (reason) => ` [ERROR] Diff impossible: ${reason}\n` +
118
+ ` → Re-read the file to check the doc against the current code, then call refresh_file_snapshot.`;
119
+ if ("error" in base)
120
+ return impossible(base.error);
121
+ const verdict = matchesSnapshot(base.content, tracked);
122
+ if (verdict === false) {
123
+ return impossible(`the content at ${base.origin} does not match the hash recorded at the snapshot.`);
124
+ }
125
+ const unverifiedNote = verdict === undefined ? ` [INFO] No hash recorded for this entry: ${base.origin} could not be checked against the snapshot.\n\n` : "";
126
+ const inferredNote = base.inferred && verdict ? ` [INFO] Base: ${base.origin} (no source commit recorded), checked against the snapshot hash.\n\n` : "";
127
+ let current;
128
+ try {
129
+ current = await source.read(tracked.path);
130
+ }
131
+ catch (err) {
132
+ return `${unverifiedNote}${inferredNote} [ERROR] The current content cannot be read from ${source.label}: ${errorMessage(err)}`;
113
133
  }
114
- return diffs;
134
+ if (!current)
135
+ return `${unverifiedNote}${inferredNote} [INFO] The file no longer exists at ${source.label}.`;
136
+ const before = base.content.lines.join("\n");
137
+ const after = current.content.lines.join("\n");
138
+ if (before === after)
139
+ return `${unverifiedNote}${inferredNote} [INFO] No changes since the snapshot.`;
140
+ const patch = createTwoFilesPatch(`a/${tracked.path}`, `b/${tracked.path}`, before, after, base.origin, "current", { context: 3 });
141
+ return unverifiedNote + inferredNote + patch.replace(/^=+\n/, "").trimEnd();
115
142
  }
116
143
  export async function gitDiffDoc(storageEngine, args) {
117
144
  try {
@@ -121,7 +148,6 @@ export async function gitDiffDoc(storageEngine, args) {
121
148
  if (projectError)
122
149
  return scopeErrorResult(projectError);
123
150
  const source = getCodeSource(project);
124
- const githubApi = getGitHubApi(project);
125
151
  const wanted = validated.file_paths
126
152
  ? new Set(await Promise.all(validated.file_paths.map((p) => toStoredPathForMatching(project, p))))
127
153
  : undefined;
@@ -165,11 +191,10 @@ export async function gitDiffDoc(storageEngine, args) {
165
191
  docSections.push(`## ${doc.title} [${docPath}]\n\n[INFO] All tracked files already shown in a previous documentation section.`);
166
192
  continue;
167
193
  }
168
- const diffs = githubApi ? await getGitHubDiffs(githubApi, fresh) : undefined;
169
- const fileSections = fresh.map((tf) => {
170
- const diff = diffs ? diffs.get(tf.path) : getLocalGitDiff(tf, project);
171
- const base = tf.sourceRevision ? `\nSnapshot commit: ${tf.sourceRevision.slice(0, 7)}` : "";
172
- return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}${base}\n\n${diff}`;
194
+ const diffs = await mapWithConcurrency(fresh, FILE_CONCURRENCY, (tf) => diffTrackedFile(tf, project, source));
195
+ const fileSections = fresh.map((tf, i) => {
196
+ const base = tf.sourceRevision ? `\nSnapshot commit: ${short(tf.sourceRevision)}` : "";
197
+ return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}${base}\n\n${diffs[i]}`;
173
198
  });
174
199
  docSections.push(`## ${doc.title} [${docPath}]\n\nCode source: ${source.label}\n\n${fileSections.join("\n\n---\n\n")}`);
175
200
  }
@@ -26,7 +26,8 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
26
26
  }, REPLACEMENT_PARAMS);
27
27
  const search = searchSource.text;
28
28
  const { content: replacement, additionalTrackedFiles } = await resolveCodeRefs(replacementSource.text, resolvedProject);
29
- if (!doc.content.includes(search)) {
29
+ const occurrences = search === "" ? 0 : doc.content.split(search).length - 1;
30
+ if (occurrences === 0) {
30
31
  const preview = doc.content.slice(0, 300).replace(/\n/g, "↵");
31
32
  return {
32
33
  isError: true,
@@ -36,7 +37,17 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
36
37
  }],
37
38
  };
38
39
  }
39
- doc.content = doc.content.replace(search, replacement);
40
+ if (occurrences > 1 && !validated.replace_all) {
41
+ return {
42
+ isError: true,
43
+ content: [{
44
+ type: "text",
45
+ text: `The text to replace occurs ${occurrences} times in "${validated.path}" (project: "${resolvedProject}") — nothing patched.\nExtend the search text with surrounding context so it matches once, or pass replace_all: true to replace every occurrence.`,
46
+ }],
47
+ };
48
+ }
49
+ // split/join rather than String.replace: the replacement is inserted verbatim, `$&` and `$1` included.
50
+ doc.content = doc.content.split(search).join(replacement);
40
51
  doc.tracked_files = await mergeTrackedFiles(doc.tracked_files, additionalTrackedFiles, resolvedProject);
41
52
  doc.updatedAt = new Date().toISOString();
42
53
  await storageEngine.saveDoc(doc, resolvedProject, "patch");
@@ -51,7 +62,7 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
51
62
  if (replacementSource.draft)
52
63
  deleteDraft(dataDir, replacementSource.draft);
53
64
  return {
54
- content: [{ type: "text", text: `Documentation entry "${validated.path}" patched successfully.` }],
65
+ content: [{ type: "text", text: `Documentation entry "${validated.path}" patched successfully: ${occurrences} occurrence(s) replaced.` }],
55
66
  };
56
67
  }
57
68
  catch (err) {
@@ -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-DPGmAc6z.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};
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-DvVxuv5N.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};