march-cli 0.1.23 → 0.1.24

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="docs/assets/march-banner.png" alt="March CLI banner" width="800">
2
+ <img src="docs/assets/march-banner.svg" alt="March CLI banner" width="800">
3
3
  </p>
4
4
 
5
5
  <p align="center"><strong>Forget Skills. Embrace Memory. Keep your model in the sweet spot.</strong></p>
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="docs/assets/march-banner.png" alt="March CLI banner" width="800">
2
+ <img src="docs/assets/march-banner.svg" alt="March CLI banner" width="800">
3
3
  </p>
4
4
 
5
5
  <p align="center"><strong>忘记 Skill,拥抱记忆,让你的模型永远工作在甜点区。</strong></p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -19,6 +19,9 @@
19
19
  "test:shell-runtime-real": "node test/shell-real-runtime.acceptance.mjs",
20
20
  "test:shell-tui-real": "node test/shell-tui-real.acceptance.mjs",
21
21
  "test:tui-key-real": "node test/tui-key-real.acceptance.mjs",
22
+ "docs:dev": "vitepress dev docs --host 127.0.0.1",
23
+ "docs:build": "vitepress build docs",
24
+ "docs:preview": "vitepress preview docs --host 127.0.0.1",
22
25
  "context": "cd .. && node march-cli/bin/march.mjs --dump-context",
23
26
  "notify:experiment": "node scripts/notify-experiment.mjs",
24
27
  "publish:env": "node scripts/npm-publish-from-env.mjs"
@@ -39,5 +42,8 @@
39
42
  },
40
43
  "optionalDependencies": {
41
44
  "@vscode/ripgrep": "^1.18.0"
45
+ },
46
+ "devDependencies": {
47
+ "vitepress": "^1.6.4"
42
48
  }
43
49
  }
@@ -0,0 +1,57 @@
1
+ import { extname, normalize } from "node:path";
2
+
3
+ const MAX_COHESIVE_LINES = 300;
4
+ const MIN_ADDED_LINES = 40;
5
+
6
+ const CODE_EXTENSIONS = new Set([
7
+ ".cjs",
8
+ ".go",
9
+ ".js",
10
+ ".jsx",
11
+ ".mjs",
12
+ ".py",
13
+ ".rs",
14
+ ".ts",
15
+ ".tsx",
16
+ ]);
17
+
18
+ const IGNORED_SEGMENTS = new Set([
19
+ "build",
20
+ "coverage",
21
+ "dist",
22
+ "fixture",
23
+ "fixtures",
24
+ "node_modules",
25
+ "vendor",
26
+ ]);
27
+
28
+ export function buildCohesionWarning({ path, oldText, newText }) {
29
+ if (!shouldCheckPath(path)) return null;
30
+
31
+ const oldLines = countLines(oldText);
32
+ const newLines = countLines(newText);
33
+ const addedLines = Math.max(0, newLines - oldLines);
34
+ if (newLines <= MAX_COHESIVE_LINES || addedLines < MIN_ADDED_LINES) return null;
35
+
36
+ return [
37
+ "[cohesion]",
38
+ `${formatPath(path)} is now ${newLines} lines after a +${addedLines} line edit.`,
39
+ "If the added logic is a separate responsibility, extract it into a cohesive module before continuing.",
40
+ ].join("\n");
41
+ }
42
+
43
+ function shouldCheckPath(path) {
44
+ const normalized = normalize(path).replaceAll("\\", "/");
45
+ if (normalized.includes(".generated.")) return false;
46
+ if (!CODE_EXTENSIONS.has(extname(normalized))) return false;
47
+ return !normalized.split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
48
+ }
49
+
50
+ function countLines(text) {
51
+ if (!text) return 0;
52
+ return String(text).split("\n").length;
53
+ }
54
+
55
+ function formatPath(path) {
56
+ return String(path).replaceAll("\\", "/");
57
+ }
@@ -4,8 +4,9 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
5
  import { toolText } from "./tool-result.mjs";
6
6
  import { applyReplaceTextPatch, applyReplaceRangePatch } from "./editing/diff-apply.mjs";
7
- import { formatAppliedDiff, formatDiff } from "./editing/diff-format.mjs";
8
- import { waitForLspReport } from "./editing/lsp-report.mjs";
7
+ import { buildCohesionWarning } from "./editing/cohesion-warning.mjs";
8
+ import { formatAppliedDiff, formatDiff } from "./editing/diff-format.mjs";
9
+ import { waitForLspReport } from "./editing/lsp-report.mjs";
9
10
 
10
11
  export { formatDiff } from "./editing/diff-format.mjs";
11
12
 
@@ -67,8 +68,9 @@ async function writeFullFile({ absPath, path, content, mode, engine, ui, lspServ
67
68
  if (mode === WRITE_MODE && existsSync(absPath)) {
68
69
  return toolText(`Error: ${absPath} already exists. Use mode=overwrite to replace it.`, { error: true });
69
70
  }
70
- const oldText = mode === OVERWRITE_MODE && existsSync(absPath) ? readFileSync(absPath, "utf8") : "";
71
- const diffLines = formatDiff(oldText, content, { startLine: 1 });
71
+ const oldText = mode === OVERWRITE_MODE && existsSync(absPath) ? readFileSync(absPath, "utf8") : "";
72
+ const cohesionWarning = buildCohesionWarning({ path: absPath, oldText, newText: content });
73
+ const diffLines = formatDiff(oldText, content, { startLine: 1 });
72
74
  try {
73
75
  mkdirSync(dirname(absPath), { recursive: true });
74
76
  writeFileSync(absPath, content, "utf8");
@@ -76,7 +78,8 @@ async function writeFullFile({ absPath, path, content, mode, engine, ui, lspServ
76
78
  const lspResult = await lspService?.touchFile?.(absPath);
77
79
  ui.editDiff(absPath, diffLines);
78
80
 
79
- return await toolTextWithDiagnostics(`${mode === WRITE_MODE ? "Wrote" : "Overwrote"} ${path}\n\n${formatAppliedDiff([{ oldText, newText: content, startLine: 1 }])}`, { path: absPath }, { lspService, path: absPath, lspResult, since: lspTouchedAt });
81
+ const baseText = `${mode === WRITE_MODE ? "Wrote" : "Overwrote"} ${path}\n\n${formatAppliedDiff([{ oldText, newText: content, startLine: 1 }])}`;
82
+ return await toolTextWithDiagnostics(appendSections(baseText, [cohesionWarning]), withWarnings({ path: absPath }, cohesionWarning), { lspService, path: absPath, lspResult, since: lspTouchedAt });
80
83
  } catch (err) {
81
84
  return toolText(`Error writing ${absPath}: ${err.message}`, { error: true });
82
85
  }
@@ -97,23 +100,35 @@ async function patchFile({ absPath, path, edits, engine, ui, lspService }) {
97
100
  const prepared = preparePatchEdits(content, edits, absPath);
98
101
  if (prepared.error) return toolText(prepared.error, { error: true });
99
102
 
100
- const newContent = applyPreparedEdits(content, prepared.edits);
101
- try {
103
+ const newContent = applyPreparedEdits(content, prepared.edits);
104
+ const cohesionWarning = buildCohesionWarning({ path: absPath, oldText: content, newText: newContent });
105
+ try {
102
106
  mkdirSync(dirname(absPath), { recursive: true });
103
107
  writeFileSync(absPath, newContent, "utf8");
104
108
  const lspTouchedAt = Date.now();
105
109
  const lspResult = await lspService?.touchFile?.(absPath);
106
110
  ui.editDiff(absPath, prepared.edits.flatMap((edit) => formatDiff(edit.oldText, edit.newText, { startLine: edit.startLine })));
107
- return await toolTextWithDiagnostics(`Edited ${absPath}\n\n${formatAppliedDiff(prepared.edits)}`, { path: absPath, edits: prepared.edits.length }, { lspService, path: absPath, lspResult, since: lspTouchedAt });
111
+ const baseText = `Edited ${absPath}\n\n${formatAppliedDiff(prepared.edits)}`;
112
+ const details = withWarnings({ path: absPath, edits: prepared.edits.length }, cohesionWarning);
113
+ return await toolTextWithDiagnostics(appendSections(baseText, [cohesionWarning]), details, { lspService, path: absPath, lspResult, since: lspTouchedAt });
108
114
  } catch (err) {
109
115
  return toolText(`Error writing ${absPath}: ${err.message}`, { error: true });
110
116
  }
111
117
  }
112
118
 
113
- async function toolTextWithDiagnostics(text, details, { lspService, path, lspResult, since = Date.now(), timeoutMs = 3000, intervalMs = 150 } = {}) {
114
- const lspReport = await waitForLspReport({ lspService, path, lspResult, since, timeoutMs, intervalMs });
115
- return toolText(lspReport ? `${text}\n\n${lspReport}` : text, details);
116
- }
119
+ async function toolTextWithDiagnostics(text, details, { lspService, path, lspResult, since = Date.now(), timeoutMs = 3000, intervalMs = 150 } = {}) {
120
+ const lspReport = await waitForLspReport({ lspService, path, lspResult, since, timeoutMs, intervalMs });
121
+ return toolText(lspReport ? `${text}\n\n${lspReport}` : text, details);
122
+ }
123
+
124
+ function appendSections(text, sections) {
125
+ return [text, ...sections.filter(Boolean)].join("\n\n");
126
+ }
127
+
128
+ function withWarnings(details, cohesionWarning) {
129
+ if (!cohesionWarning) return details;
130
+ return { ...details, warnings: ["cohesion"] };
131
+ }
117
132
 
118
133
  export function preparePatchEdits(content, edits, path = "file") {
119
134
  const prepared = [];