@coreyuan/vector-mind 1.0.44 → 1.0.49
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 +139 -376
- package/dist/builtin-conventions.js +17 -2
- package/dist/builtin-conventions.js.map +1 -1
- package/dist/builtin-instructions.d.ts +2 -1
- package/dist/builtin-instructions.js +2 -1
- package/dist/builtin-instructions.js.map +1 -1
- package/dist/index.js +1032 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/builtin-policy.d.ts +0 -7
- package/dist/builtin-policy.js +0 -24
- package/dist/builtin-policy.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -14,9 +14,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
14
14
|
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
15
15
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
16
16
|
import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
|
|
17
|
-
import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
|
|
17
|
+
import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_REQUIREMENT_BOUNDARY_AND_MODULARITY_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
|
|
18
18
|
const SERVER_NAME = "vector-mind";
|
|
19
|
-
const SERVER_VERSION = "1.0.
|
|
19
|
+
const SERVER_VERSION = "1.0.49";
|
|
20
20
|
const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
|
|
21
21
|
const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
|
|
22
22
|
const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
|
|
@@ -65,6 +65,52 @@ const PENDING_PRUNE_EVERY = (() => {
|
|
|
65
65
|
return 500;
|
|
66
66
|
return n;
|
|
67
67
|
})();
|
|
68
|
+
const DEVELOPMENT_WARN_FILE_LINES = (() => {
|
|
69
|
+
const raw = process.env.VECTORMIND_WARN_FILE_LINES?.trim();
|
|
70
|
+
if (!raw)
|
|
71
|
+
return 800;
|
|
72
|
+
const n = Number.parseInt(raw, 10);
|
|
73
|
+
if (!Number.isFinite(n) || n < 100)
|
|
74
|
+
return 800;
|
|
75
|
+
return Math.min(50_000, n);
|
|
76
|
+
})();
|
|
77
|
+
const DEVELOPMENT_BLOCK_FILE_LINES = (() => {
|
|
78
|
+
const raw = process.env.VECTORMIND_BLOCK_FILE_LINES?.trim();
|
|
79
|
+
if (!raw)
|
|
80
|
+
return 1200;
|
|
81
|
+
const n = Number.parseInt(raw, 10);
|
|
82
|
+
if (!Number.isFinite(n) || n < DEVELOPMENT_WARN_FILE_LINES)
|
|
83
|
+
return Math.max(1200, DEVELOPMENT_WARN_FILE_LINES);
|
|
84
|
+
return Math.min(100_000, n);
|
|
85
|
+
})();
|
|
86
|
+
const DEVELOPMENT_HUGE_FILE_LINES = (() => {
|
|
87
|
+
const raw = process.env.VECTORMIND_HUGE_FILE_LINES?.trim();
|
|
88
|
+
if (!raw)
|
|
89
|
+
return 3000;
|
|
90
|
+
const n = Number.parseInt(raw, 10);
|
|
91
|
+
if (!Number.isFinite(n) || n < DEVELOPMENT_BLOCK_FILE_LINES) {
|
|
92
|
+
return Math.max(3000, DEVELOPMENT_BLOCK_FILE_LINES);
|
|
93
|
+
}
|
|
94
|
+
return Math.min(200_000, n);
|
|
95
|
+
})();
|
|
96
|
+
const DEVELOPMENT_WARN_FILE_BYTES = (() => {
|
|
97
|
+
const raw = process.env.VECTORMIND_WARN_FILE_BYTES?.trim();
|
|
98
|
+
if (!raw)
|
|
99
|
+
return 120_000;
|
|
100
|
+
const n = Number.parseInt(raw, 10);
|
|
101
|
+
if (!Number.isFinite(n) || n < 10_000)
|
|
102
|
+
return 120_000;
|
|
103
|
+
return Math.min(20_000_000, n);
|
|
104
|
+
})();
|
|
105
|
+
const DEVELOPMENT_WARN_PENDING_FILES = (() => {
|
|
106
|
+
const raw = process.env.VECTORMIND_WARN_PENDING_FILES?.trim();
|
|
107
|
+
if (!raw)
|
|
108
|
+
return 12;
|
|
109
|
+
const n = Number.parseInt(raw, 10);
|
|
110
|
+
if (!Number.isFinite(n) || n < 1)
|
|
111
|
+
return 12;
|
|
112
|
+
return Math.min(500, n);
|
|
113
|
+
})();
|
|
68
114
|
const RIPGREP_RESOLVE_TIMEOUT_MS = 5_000;
|
|
69
115
|
const RIPGREP_SEARCH_TIMEOUT_MS = 30_000;
|
|
70
116
|
const RIPGREP_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
@@ -1384,6 +1430,470 @@ function isContentIndexableFile(filePath) {
|
|
|
1384
1430
|
return false;
|
|
1385
1431
|
return getContentChunkKind(filePath) !== null;
|
|
1386
1432
|
}
|
|
1433
|
+
function isLikelySourceImplementationFile(filePath) {
|
|
1434
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1435
|
+
return new Set([
|
|
1436
|
+
".ts",
|
|
1437
|
+
".tsx",
|
|
1438
|
+
".js",
|
|
1439
|
+
".jsx",
|
|
1440
|
+
".mjs",
|
|
1441
|
+
".cjs",
|
|
1442
|
+
".py",
|
|
1443
|
+
".go",
|
|
1444
|
+
".rs",
|
|
1445
|
+
".java",
|
|
1446
|
+
".kt",
|
|
1447
|
+
".cs",
|
|
1448
|
+
".c",
|
|
1449
|
+
".cc",
|
|
1450
|
+
".cpp",
|
|
1451
|
+
".h",
|
|
1452
|
+
".hpp",
|
|
1453
|
+
".vue",
|
|
1454
|
+
".svelte",
|
|
1455
|
+
]).has(ext);
|
|
1456
|
+
}
|
|
1457
|
+
function countFileLinesBounded(absPath, maxBytes) {
|
|
1458
|
+
let stat;
|
|
1459
|
+
try {
|
|
1460
|
+
stat = fs.statSync(absPath);
|
|
1461
|
+
}
|
|
1462
|
+
catch {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
if (!stat.isFile())
|
|
1466
|
+
return null;
|
|
1467
|
+
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
1468
|
+
const fd = fs.openSync(absPath, "r");
|
|
1469
|
+
try {
|
|
1470
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
1471
|
+
const read = fs.readSync(fd, buffer, 0, bytesToRead, 0);
|
|
1472
|
+
let lines = read > 0 ? 1 : 0;
|
|
1473
|
+
for (let i = 0; i < read; i++) {
|
|
1474
|
+
if (buffer[i] === 10)
|
|
1475
|
+
lines += 1;
|
|
1476
|
+
}
|
|
1477
|
+
return { lines, truncated: stat.size > bytesToRead };
|
|
1478
|
+
}
|
|
1479
|
+
finally {
|
|
1480
|
+
fs.closeSync(fd);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
function isPathInsideProjectRoot(absPath) {
|
|
1484
|
+
const root = path.resolve(projectRoot);
|
|
1485
|
+
const rel = path.relative(root, path.resolve(absPath));
|
|
1486
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
1487
|
+
}
|
|
1488
|
+
function checkPathScope(inputPath) {
|
|
1489
|
+
const normalizedInput = inputPath.trim() || ".";
|
|
1490
|
+
const absPath = path.resolve(path.isAbsolute(normalizedInput) ? normalizedInput : path.join(projectRoot, normalizedInput));
|
|
1491
|
+
return {
|
|
1492
|
+
input_path: inputPath,
|
|
1493
|
+
abs_path: absPath,
|
|
1494
|
+
in_project: isPathInsideProjectRoot(absPath),
|
|
1495
|
+
project_root: path.resolve(projectRoot),
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function buildCrossProjectPathWarnings(paths) {
|
|
1499
|
+
const checks = (paths ?? []).map((p) => checkPathScope(p)).filter((c) => !c.in_project);
|
|
1500
|
+
if (!checks.length)
|
|
1501
|
+
return [];
|
|
1502
|
+
return [
|
|
1503
|
+
{
|
|
1504
|
+
code: "cross_project_path",
|
|
1505
|
+
severity: "warning",
|
|
1506
|
+
message: "A path points outside the current project_root. Switch project_root intentionally before reading/searching another repo; do not mix unrelated project context into the current requirement.",
|
|
1507
|
+
files: checks.slice(0, 10).map((c) => c.input_path),
|
|
1508
|
+
details: {
|
|
1509
|
+
project_root: path.resolve(projectRoot),
|
|
1510
|
+
paths: checks.slice(0, 10),
|
|
1511
|
+
total_paths: checks.length,
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
];
|
|
1515
|
+
}
|
|
1516
|
+
function buildLargeImplementationFileWarning(args) {
|
|
1517
|
+
const linesValue = args.lineCountTruncated ? `${args.lineCount}+` : args.lineCount;
|
|
1518
|
+
if (args.lineCount >= DEVELOPMENT_HUGE_FILE_LINES) {
|
|
1519
|
+
return {
|
|
1520
|
+
code: "huge_file_modularization_required",
|
|
1521
|
+
severity: "blocker",
|
|
1522
|
+
message: "This implementation file is huge. Before any normal feature work, perform mechanical modularization: move whole functions/types/impl blocks into real, clearly named modules/directories, avoid *.generated.* or *.parts files, preserve behavior, then run format/build/tests.",
|
|
1523
|
+
files: [args.filePath],
|
|
1524
|
+
details: {
|
|
1525
|
+
lines: linesValue,
|
|
1526
|
+
bytes: args.bytes,
|
|
1527
|
+
warn_lines: DEVELOPMENT_WARN_FILE_LINES,
|
|
1528
|
+
block_lines: DEVELOPMENT_BLOCK_FILE_LINES,
|
|
1529
|
+
huge_lines: DEVELOPMENT_HUGE_FILE_LINES,
|
|
1530
|
+
required_action: "mechanical_modularization",
|
|
1531
|
+
allowed_change_modes: ["mechanical_modularization", "emergency_hotfix"],
|
|
1532
|
+
forbidden_file_patterns: ["*.generated.*", "*.parts", "*.rs.parts", "*_part*"],
|
|
1533
|
+
mechanical_rules: [
|
|
1534
|
+
"move whole declarations only",
|
|
1535
|
+
"use real module names and clear directory boundaries",
|
|
1536
|
+
"preserve behavior and public semantics",
|
|
1537
|
+
"only add necessary mod/use/pub(crate)/re-export glue",
|
|
1538
|
+
"run formatter, build, and tests after each phase",
|
|
1539
|
+
],
|
|
1540
|
+
reading: !!args.reading,
|
|
1541
|
+
},
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
code: args.code,
|
|
1546
|
+
severity: args.code === "large_file" || (args.code === "large_file_read" && args.lineCount < DEVELOPMENT_BLOCK_FILE_LINES)
|
|
1547
|
+
? "warning"
|
|
1548
|
+
: "blocker",
|
|
1549
|
+
message: args.code === "large_file"
|
|
1550
|
+
? "This implementation file is getting large. Prefer extracting focused modules instead of continuing to pile unrelated responsibilities into it."
|
|
1551
|
+
: args.reading
|
|
1552
|
+
? "You are reading a very large implementation file. Do not keep patching new feature code into it; identify a narrow function and split new behavior into focused modules unless this task is explicitly a planned extraction."
|
|
1553
|
+
: "This implementation file is already very large. Do not add new feature code here by default; split into a focused module/service/component and keep this file as a thin entry.",
|
|
1554
|
+
files: [args.filePath],
|
|
1555
|
+
details: {
|
|
1556
|
+
lines: linesValue,
|
|
1557
|
+
bytes: args.bytes,
|
|
1558
|
+
warn_lines: DEVELOPMENT_WARN_FILE_LINES,
|
|
1559
|
+
block_lines: DEVELOPMENT_BLOCK_FILE_LINES,
|
|
1560
|
+
huge_lines: DEVELOPMENT_HUGE_FILE_LINES,
|
|
1561
|
+
},
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function buildFileReadDevelopmentWarnings(filePath, absPath, stat) {
|
|
1565
|
+
const warnings = [];
|
|
1566
|
+
if (!isPathInsideProjectRoot(absPath)) {
|
|
1567
|
+
warnings.push(...buildCrossProjectPathWarnings([filePath]));
|
|
1568
|
+
return warnings;
|
|
1569
|
+
}
|
|
1570
|
+
if (!isLikelySourceImplementationFile(filePath))
|
|
1571
|
+
return warnings;
|
|
1572
|
+
let st = stat;
|
|
1573
|
+
try {
|
|
1574
|
+
st ??= fs.statSync(absPath);
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
return warnings;
|
|
1578
|
+
}
|
|
1579
|
+
if (!st.isFile())
|
|
1580
|
+
return warnings;
|
|
1581
|
+
const lineInfo = countFileLinesBounded(absPath, 2_000_000);
|
|
1582
|
+
const lineCount = lineInfo?.lines ?? 0;
|
|
1583
|
+
const tooManyLines = lineCount >= DEVELOPMENT_BLOCK_FILE_LINES;
|
|
1584
|
+
const warnLines = lineCount >= DEVELOPMENT_WARN_FILE_LINES;
|
|
1585
|
+
const warnBytes = st.size >= DEVELOPMENT_WARN_FILE_BYTES;
|
|
1586
|
+
if (!tooManyLines && !warnLines && !warnBytes)
|
|
1587
|
+
return warnings;
|
|
1588
|
+
warnings.push(buildLargeImplementationFileWarning({
|
|
1589
|
+
code: "large_file_read",
|
|
1590
|
+
filePath,
|
|
1591
|
+
lineCount,
|
|
1592
|
+
lineCountTruncated: lineInfo?.truncated,
|
|
1593
|
+
bytes: st.size,
|
|
1594
|
+
reading: true,
|
|
1595
|
+
}));
|
|
1596
|
+
return warnings;
|
|
1597
|
+
}
|
|
1598
|
+
function buildMatchedFileDevelopmentWarnings(filePaths) {
|
|
1599
|
+
const seen = new Set();
|
|
1600
|
+
const warnings = [];
|
|
1601
|
+
for (const fp of filePaths) {
|
|
1602
|
+
if (!fp || seen.has(fp))
|
|
1603
|
+
continue;
|
|
1604
|
+
seen.add(fp);
|
|
1605
|
+
const abs = path.isAbsolute(fp) ? path.resolve(fp) : path.join(projectRoot, fp);
|
|
1606
|
+
warnings.push(...buildFileReadDevelopmentWarnings(normalizeToDbPath(fp), abs));
|
|
1607
|
+
if (warnings.length >= 8)
|
|
1608
|
+
break;
|
|
1609
|
+
}
|
|
1610
|
+
return warnings;
|
|
1611
|
+
}
|
|
1612
|
+
function buildRequirementStartWarnings(args) {
|
|
1613
|
+
const warnings = [];
|
|
1614
|
+
const activeReqs = listActiveRequirementsStmt.all(10);
|
|
1615
|
+
if (!args.close_previous && activeReqs.length > 0) {
|
|
1616
|
+
warnings.push({
|
|
1617
|
+
code: "multiple_active_requirements",
|
|
1618
|
+
severity: "warning",
|
|
1619
|
+
message: "Starting a requirement without closing previous active requirements can mix unrelated context. Only keep multiple active requirements when the user explicitly asked for parallel work.",
|
|
1620
|
+
details: {
|
|
1621
|
+
active_requirements: activeReqs.slice(0, 5).map((r) => ({ id: r.id, title: r.title, status: r.status })),
|
|
1622
|
+
},
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
const text = `${args.title}\n${args.background}`.toLowerCase();
|
|
1626
|
+
const broadTerms = [
|
|
1627
|
+
"顺便",
|
|
1628
|
+
"一起",
|
|
1629
|
+
"所有",
|
|
1630
|
+
"全部",
|
|
1631
|
+
"整体",
|
|
1632
|
+
"重构",
|
|
1633
|
+
"统一",
|
|
1634
|
+
"优化一下",
|
|
1635
|
+
"顺手",
|
|
1636
|
+
"相关的",
|
|
1637
|
+
"all ",
|
|
1638
|
+
"everything",
|
|
1639
|
+
"refactor",
|
|
1640
|
+
"cleanup",
|
|
1641
|
+
"clean up",
|
|
1642
|
+
];
|
|
1643
|
+
const matched = broadTerms.filter((term) => text.includes(term));
|
|
1644
|
+
if (matched.length >= 2 || text.length > 1800) {
|
|
1645
|
+
warnings.push({
|
|
1646
|
+
code: "broad_requirement_scope",
|
|
1647
|
+
severity: "warning",
|
|
1648
|
+
message: "The requirement wording looks broad. Treat the current user request as the only boundary; do not add extra workflows, fields, pages, interfaces, or touch completed related features unless explicitly required.",
|
|
1649
|
+
details: { matched_terms: matched.slice(0, 10), text_length: text.length },
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
return warnings;
|
|
1653
|
+
}
|
|
1654
|
+
function normalizeScopeTerms(values) {
|
|
1655
|
+
return Array.from(new Set((values ?? []).map((v) => v.trim()).filter(Boolean)));
|
|
1656
|
+
}
|
|
1657
|
+
function buildRequirementScopeContract(args) {
|
|
1658
|
+
const allowTerms = normalizeScopeTerms(args.scope_allow);
|
|
1659
|
+
const denyTerms = normalizeScopeTerms(args.scope_deny);
|
|
1660
|
+
const allowedPaths = normalizeScopeTerms(args.allowed_paths).map(normalizeToDbPath);
|
|
1661
|
+
const deniedPaths = normalizeScopeTerms(args.denied_paths).map((p) => p.replace(/\\/g, "/"));
|
|
1662
|
+
return {
|
|
1663
|
+
allow_terms: allowTerms,
|
|
1664
|
+
deny_terms: Array.from(new Set(denyTerms)),
|
|
1665
|
+
allowed_paths: Array.from(new Set(allowedPaths)),
|
|
1666
|
+
denied_paths: Array.from(new Set(deniedPaths)),
|
|
1667
|
+
inferred_from: [],
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function getRequirementScopeContract(reqId) {
|
|
1671
|
+
const memId = getRequirementMemoryItemIdStmt.get(reqId)?.id;
|
|
1672
|
+
if (memId == null)
|
|
1673
|
+
return null;
|
|
1674
|
+
const row = getMemoryItemByIdStmt.get(memId);
|
|
1675
|
+
const meta = parseMetadataJson(row?.metadata_json);
|
|
1676
|
+
const raw = meta.scope_contract;
|
|
1677
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1678
|
+
return null;
|
|
1679
|
+
const obj = raw;
|
|
1680
|
+
return {
|
|
1681
|
+
allow_terms: Array.isArray(obj.allow_terms) ? obj.allow_terms.filter((v) => typeof v === "string") : [],
|
|
1682
|
+
deny_terms: Array.isArray(obj.deny_terms) ? obj.deny_terms.filter((v) => typeof v === "string") : [],
|
|
1683
|
+
allowed_paths: Array.isArray(obj.allowed_paths) ? obj.allowed_paths.filter((v) => typeof v === "string") : [],
|
|
1684
|
+
denied_paths: Array.isArray(obj.denied_paths) ? obj.denied_paths.filter((v) => typeof v === "string") : [],
|
|
1685
|
+
inferred_from: Array.isArray(obj.inferred_from) ? obj.inferred_from.filter((v) => typeof v === "string") : [],
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
function wildcardToRegex(pattern) {
|
|
1689
|
+
const source = escapeRegExp(pattern).replace(/\\\*/g, ".*");
|
|
1690
|
+
return new RegExp(source, "i");
|
|
1691
|
+
}
|
|
1692
|
+
function pathMatchesAnyPattern(filePath, patterns) {
|
|
1693
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
1694
|
+
return patterns.filter((p) => wildcardToRegex(p.replace(/\\/g, "/")).test(normalized));
|
|
1695
|
+
}
|
|
1696
|
+
function fileContentHasDeniedTerms(filePath, terms) {
|
|
1697
|
+
if (!terms.length || filePath === "(unspecified)")
|
|
1698
|
+
return [];
|
|
1699
|
+
const abs = path.isAbsolute(filePath) ? path.resolve(filePath) : path.join(projectRoot, filePath);
|
|
1700
|
+
let st;
|
|
1701
|
+
try {
|
|
1702
|
+
st = fs.statSync(abs);
|
|
1703
|
+
}
|
|
1704
|
+
catch {
|
|
1705
|
+
return [];
|
|
1706
|
+
}
|
|
1707
|
+
if (!st.isFile() || st.size > 2_000_000)
|
|
1708
|
+
return [];
|
|
1709
|
+
let content = "";
|
|
1710
|
+
try {
|
|
1711
|
+
content = fs.readFileSync(abs, "utf8");
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
const lower = `${filePath}\n${content.slice(0, 250_000)}`.toLowerCase();
|
|
1717
|
+
return terms.filter((term) => lower.includes(term.toLowerCase()));
|
|
1718
|
+
}
|
|
1719
|
+
function mergeScopeContracts(base, extra) {
|
|
1720
|
+
if (!base && !extra)
|
|
1721
|
+
return null;
|
|
1722
|
+
return {
|
|
1723
|
+
allow_terms: Array.from(new Set([...(base?.allow_terms ?? []), ...(extra?.allow_terms ?? [])])),
|
|
1724
|
+
deny_terms: Array.from(new Set([...(base?.deny_terms ?? []), ...(extra?.deny_terms ?? [])])),
|
|
1725
|
+
allowed_paths: Array.from(new Set([...(base?.allowed_paths ?? []), ...(extra?.allowed_paths ?? [])])),
|
|
1726
|
+
denied_paths: Array.from(new Set([...(base?.denied_paths ?? []), ...(extra?.denied_paths ?? [])])),
|
|
1727
|
+
inferred_from: Array.from(new Set([...(base?.inferred_from ?? []), ...(extra?.inferred_from ?? [])])),
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function buildScopeDriftWarnings(args) {
|
|
1731
|
+
const requirementContract = args.requirement ? getRequirementScopeContract(args.requirement.id) : null;
|
|
1732
|
+
const contract = mergeScopeContracts(requirementContract, args.contract);
|
|
1733
|
+
const hasScopeRules = !!contract && (contract.allow_terms.length > 0 ||
|
|
1734
|
+
contract.deny_terms.length > 0 ||
|
|
1735
|
+
contract.allowed_paths.length > 0 ||
|
|
1736
|
+
contract.denied_paths.length > 0);
|
|
1737
|
+
const warnings = [];
|
|
1738
|
+
if (!contract || !hasScopeRules) {
|
|
1739
|
+
if (args.includeMissingContractHint && args.files.length > 0) {
|
|
1740
|
+
warnings.push({
|
|
1741
|
+
code: "scope_contract_missing",
|
|
1742
|
+
severity: "warning",
|
|
1743
|
+
message: "No explicit scope allow/deny contract is set for this requirement, so the planned files cannot be proven in-scope before editing. Define scope_allow/scope_deny or allowed_paths/denied_paths before editing.",
|
|
1744
|
+
files: args.files.map((f) => normalizeToDbPath(f.file_path)).slice(0, 12),
|
|
1745
|
+
details: {
|
|
1746
|
+
requirement_id: args.requirement?.id ?? null,
|
|
1747
|
+
requirement_title: args.requirement?.title ?? null,
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
return warnings;
|
|
1752
|
+
}
|
|
1753
|
+
const allowTerms = contract.allow_terms;
|
|
1754
|
+
const denyTerms = contract.deny_terms;
|
|
1755
|
+
const allowedPaths = contract.allowed_paths;
|
|
1756
|
+
const deniedPaths = contract.denied_paths;
|
|
1757
|
+
const intentText = args.intent ?? "";
|
|
1758
|
+
const intentDenied = denyTerms.filter((term) => intentText.toLowerCase().includes(term.toLowerCase()));
|
|
1759
|
+
const suspicious = [];
|
|
1760
|
+
for (const f of args.files) {
|
|
1761
|
+
const fp = normalizeToDbPath(f.file_path);
|
|
1762
|
+
if (fp === "(unspecified)")
|
|
1763
|
+
continue;
|
|
1764
|
+
const matchedDeniedPaths = pathMatchesAnyPattern(fp, deniedPaths);
|
|
1765
|
+
const matchedDeniedTerms = [
|
|
1766
|
+
...denyTerms.filter((term) => fp.toLowerCase().includes(term.toLowerCase())),
|
|
1767
|
+
...fileContentHasDeniedTerms(fp, denyTerms),
|
|
1768
|
+
];
|
|
1769
|
+
const isExplicitlyAllowed = pathMatchesAnyPattern(fp, allowedPaths).length > 0 ||
|
|
1770
|
+
allowTerms.some((term) => fp.toLowerCase().includes(term.toLowerCase()));
|
|
1771
|
+
const violatesAllowedPaths = allowedPaths.length > 0 && pathMatchesAnyPattern(fp, allowedPaths).length === 0;
|
|
1772
|
+
if ((matchedDeniedPaths.length || matchedDeniedTerms.length || intentDenied.length || violatesAllowedPaths) &&
|
|
1773
|
+
!isExplicitlyAllowed) {
|
|
1774
|
+
suspicious.push({
|
|
1775
|
+
file_path: fp,
|
|
1776
|
+
matched_terms: Array.from(new Set([...matchedDeniedTerms, ...intentDenied])).slice(0, 12),
|
|
1777
|
+
matched_paths: [
|
|
1778
|
+
...matchedDeniedPaths.slice(0, 12),
|
|
1779
|
+
...(violatesAllowedPaths ? [`outside allowed_paths: ${allowedPaths.slice(0, 5).join(", ")}`] : []),
|
|
1780
|
+
],
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (suspicious.length) {
|
|
1785
|
+
warnings.push({
|
|
1786
|
+
code: "scope_drift",
|
|
1787
|
+
severity: "blocker",
|
|
1788
|
+
message: "The current requirement appears to be touching a denied or out-of-scope domain. Stop and narrow the change unless the user explicitly expanded this requirement.",
|
|
1789
|
+
files: suspicious.slice(0, 12).map((s) => s.file_path),
|
|
1790
|
+
details: {
|
|
1791
|
+
requirement_id: args.requirement?.id ?? null,
|
|
1792
|
+
requirement_title: args.requirement?.title ?? null,
|
|
1793
|
+
inferred_from: contract.inferred_from,
|
|
1794
|
+
deny_terms: denyTerms.slice(0, 30),
|
|
1795
|
+
denied_paths: deniedPaths.slice(0, 30),
|
|
1796
|
+
suspicious: suspicious.slice(0, 12),
|
|
1797
|
+
},
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
return warnings;
|
|
1801
|
+
}
|
|
1802
|
+
function buildDevelopmentWarnings(files, opts = {}) {
|
|
1803
|
+
const warnings = [];
|
|
1804
|
+
const uniqueFiles = Array.from(new Set(files
|
|
1805
|
+
.map((f) => f.file_path)
|
|
1806
|
+
.filter((f) => !!f && f !== "(unspecified)")
|
|
1807
|
+
.map((f) => normalizeToDbPath(f))));
|
|
1808
|
+
if (opts.includeUnspecified || files.some((f) => f.file_path === "(unspecified)")) {
|
|
1809
|
+
warnings.push({
|
|
1810
|
+
code: "unspecified_change_target",
|
|
1811
|
+
severity: "warning",
|
|
1812
|
+
message: "No changed file target was captured. For development work, sync concrete files so the current requirement owns only its real changes.",
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
if (uniqueFiles.length >= DEVELOPMENT_WARN_PENDING_FILES) {
|
|
1816
|
+
warnings.push({
|
|
1817
|
+
code: "many_pending_files",
|
|
1818
|
+
severity: "warning",
|
|
1819
|
+
message: "This requirement touches many files. Re-check the user request and keep only files required by the current requirement.",
|
|
1820
|
+
files: uniqueFiles.slice(0, 20),
|
|
1821
|
+
details: { total_files: uniqueFiles.length, threshold: DEVELOPMENT_WARN_PENDING_FILES },
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
const topDirs = new Set(uniqueFiles
|
|
1825
|
+
.map((f) => f.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
1826
|
+
.filter(Boolean));
|
|
1827
|
+
if (uniqueFiles.length >= 6 && topDirs.size >= 4) {
|
|
1828
|
+
warnings.push({
|
|
1829
|
+
code: "broad_change_surface",
|
|
1830
|
+
severity: "warning",
|
|
1831
|
+
message: "Changed files span several top-level areas. Avoid modifying completed or merely related features unless the current requirement explicitly needs it.",
|
|
1832
|
+
files: uniqueFiles.slice(0, 20),
|
|
1833
|
+
details: { top_level_dirs: Array.from(topDirs).slice(0, 12), total_dirs: topDirs.size },
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
for (const relPath of uniqueFiles) {
|
|
1837
|
+
if (!isLikelySourceImplementationFile(relPath))
|
|
1838
|
+
continue;
|
|
1839
|
+
const absPath = path.isAbsolute(relPath) ? relPath : path.join(projectRoot, relPath);
|
|
1840
|
+
let stat;
|
|
1841
|
+
try {
|
|
1842
|
+
stat = fs.statSync(absPath);
|
|
1843
|
+
}
|
|
1844
|
+
catch {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
if (!stat.isFile())
|
|
1848
|
+
continue;
|
|
1849
|
+
const lineInfo = countFileLinesBounded(absPath, 2_000_000);
|
|
1850
|
+
const lineCount = lineInfo?.lines ?? 0;
|
|
1851
|
+
const tooManyLines = lineCount >= DEVELOPMENT_BLOCK_FILE_LINES;
|
|
1852
|
+
const warnLines = lineCount >= DEVELOPMENT_WARN_FILE_LINES;
|
|
1853
|
+
const warnBytes = stat.size >= DEVELOPMENT_WARN_FILE_BYTES;
|
|
1854
|
+
if (!tooManyLines && !warnLines && !warnBytes)
|
|
1855
|
+
continue;
|
|
1856
|
+
warnings.push(buildLargeImplementationFileWarning({
|
|
1857
|
+
code: tooManyLines ? "very_large_file" : "large_file",
|
|
1858
|
+
filePath: relPath,
|
|
1859
|
+
lineCount,
|
|
1860
|
+
lineCountTruncated: lineInfo?.truncated,
|
|
1861
|
+
bytes: stat.size,
|
|
1862
|
+
}));
|
|
1863
|
+
}
|
|
1864
|
+
return warnings;
|
|
1865
|
+
}
|
|
1866
|
+
function isLargeFileWarningCode(code) {
|
|
1867
|
+
return (code === "large_file" ||
|
|
1868
|
+
code === "very_large_file" ||
|
|
1869
|
+
code === "large_file_read" ||
|
|
1870
|
+
code === "huge_file_modularization_required");
|
|
1871
|
+
}
|
|
1872
|
+
function isDevelopmentWarningBlockingForChangeMode(warning, changeMode) {
|
|
1873
|
+
if (changeMode === "mechanical_modularization") {
|
|
1874
|
+
if (isLargeFileWarningCode(warning.code))
|
|
1875
|
+
return false;
|
|
1876
|
+
if (warning.code === "scope_contract_missing")
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
if (changeMode === "emergency_hotfix") {
|
|
1880
|
+
if (isLargeFileWarningCode(warning.code))
|
|
1881
|
+
return false;
|
|
1882
|
+
if (warning.code === "scope_contract_missing")
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
return warning.severity === "blocker" || warning.severity === "warning";
|
|
1886
|
+
}
|
|
1887
|
+
function compactDevelopmentWarningsText(warnings) {
|
|
1888
|
+
if (!warnings.length)
|
|
1889
|
+
return [];
|
|
1890
|
+
const lines = ["development warnings:"];
|
|
1891
|
+
for (const w of warnings.slice(0, 8)) {
|
|
1892
|
+
const files = w.files?.length ? ` files=${w.files.slice(0, 5).join(",")}` : "";
|
|
1893
|
+
lines.push(`- ${w.severity} ${w.code}: ${oneLine(w.message, 180)}${files}`);
|
|
1894
|
+
}
|
|
1895
|
+
return lines;
|
|
1896
|
+
}
|
|
1387
1897
|
function extractSymbols(filePath, content) {
|
|
1388
1898
|
const ext = path.extname(filePath).toLowerCase();
|
|
1389
1899
|
if (ext === ".py")
|
|
@@ -1541,6 +2051,142 @@ function extractCLikeSymbols(content) {
|
|
|
1541
2051
|
}
|
|
1542
2052
|
return symbols;
|
|
1543
2053
|
}
|
|
2054
|
+
function declarationRegexForExtension(ext) {
|
|
2055
|
+
switch (ext) {
|
|
2056
|
+
case ".rs":
|
|
2057
|
+
return /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:fn|struct|enum|trait|impl|mod|type|const|static)\s+([A-Za-z_][\w]*)?/;
|
|
2058
|
+
case ".go":
|
|
2059
|
+
return /^\s*(?:func|type|const|var)\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*)?/;
|
|
2060
|
+
case ".py":
|
|
2061
|
+
return /^(?:class|async\s+def|def)\s+([A-Za-z_][\w]*)/;
|
|
2062
|
+
case ".ts":
|
|
2063
|
+
case ".tsx":
|
|
2064
|
+
case ".js":
|
|
2065
|
+
case ".jsx":
|
|
2066
|
+
case ".mjs":
|
|
2067
|
+
case ".cjs":
|
|
2068
|
+
return /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)?/;
|
|
2069
|
+
default:
|
|
2070
|
+
return /^\s*(?:pub\s+)?(?:async\s+)?(?:fn|function|class|struct|enum|interface|type|const|static)\s+([A-Za-z_][\w]*)?/;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function moduleNameFromDeclaration(name, signature) {
|
|
2074
|
+
const text = `${name} ${signature}`.toLowerCase();
|
|
2075
|
+
const rules = [
|
|
2076
|
+
[/(config|setting|env)/, "config"],
|
|
2077
|
+
[/(state|store|persist)/, "state"],
|
|
2078
|
+
[/(api|client|request|response|heartbeat|activate|sync)/, "api"],
|
|
2079
|
+
[/(service|daemon|install|start|stop)/, "service"],
|
|
2080
|
+
[/(log|logger|redact)/, "logging"],
|
|
2081
|
+
[/(gui|window|dialog|form|view|button|list)/, "ui"],
|
|
2082
|
+
[/(share|disk|smb|unc|folder|directory)/, "share"],
|
|
2083
|
+
[/(repair|cleanup|probe|health)/, "maintenance"],
|
|
2084
|
+
[/(path|sanitize|normalize|host|ip|util|helper)/, "util"],
|
|
2085
|
+
[/(test|mock|fixture)/, "tests"],
|
|
2086
|
+
];
|
|
2087
|
+
for (const [pattern, moduleName] of rules) {
|
|
2088
|
+
if (pattern.test(text))
|
|
2089
|
+
return moduleName;
|
|
2090
|
+
}
|
|
2091
|
+
return "core";
|
|
2092
|
+
}
|
|
2093
|
+
function topLevelDeclarationsForPlan(content, ext, maxDecls = 160) {
|
|
2094
|
+
const decls = [];
|
|
2095
|
+
const lines = content.split(/\r?\n/);
|
|
2096
|
+
const regex = declarationRegexForExtension(ext);
|
|
2097
|
+
for (let i = 0; i < lines.length && decls.length < maxDecls; i++) {
|
|
2098
|
+
const raw = lines[i];
|
|
2099
|
+
const trimmed = raw.trim();
|
|
2100
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#"))
|
|
2101
|
+
continue;
|
|
2102
|
+
const m = raw.match(regex);
|
|
2103
|
+
if (!m)
|
|
2104
|
+
continue;
|
|
2105
|
+
const fallback = trimmed.split(/\s+/).slice(0, 3).join("_").replace(/[^\w$]+/g, "_");
|
|
2106
|
+
const name = (m[1] || fallback || `declaration_${i + 1}`).replace(/^[^A-Za-z_]+/, "") || `declaration_${i + 1}`;
|
|
2107
|
+
const kind = trimmed.split(/\s+/).find((part) => ["fn", "function", "class", "struct", "enum", "trait", "impl", "mod", "type", "const", "static", "interface"].includes(part.replace(/[({].*$/, ""))) ?? "declaration";
|
|
2108
|
+
decls.push({
|
|
2109
|
+
line: i + 1,
|
|
2110
|
+
kind,
|
|
2111
|
+
name,
|
|
2112
|
+
signature: oneLine(trimmed, 180),
|
|
2113
|
+
suggested_module: moduleNameFromDeclaration(name, trimmed),
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
return decls;
|
|
2117
|
+
}
|
|
2118
|
+
function targetPathForModule(originalFilePath, targetDir, moduleName) {
|
|
2119
|
+
const ext = path.extname(originalFilePath) || ".txt";
|
|
2120
|
+
const normalizedTargetDir = targetDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2121
|
+
if (!normalizedTargetDir || normalizedTargetDir === ".")
|
|
2122
|
+
return `${moduleName}${ext}`;
|
|
2123
|
+
return `${normalizedTargetDir}/${moduleName}${ext}`;
|
|
2124
|
+
}
|
|
2125
|
+
function buildLargeFileSplitPlan(args) {
|
|
2126
|
+
const content = fs.readFileSync(args.absPath, "utf8");
|
|
2127
|
+
const st = fs.statSync(args.absPath);
|
|
2128
|
+
const lines = content.split(/\r?\n/).length;
|
|
2129
|
+
const ext = path.extname(args.absPath).toLowerCase();
|
|
2130
|
+
const baseName = path.basename(args.filePath, path.extname(args.filePath));
|
|
2131
|
+
const parentDir = path.dirname(args.filePath).replace(/\\/g, "/");
|
|
2132
|
+
const defaultTargetDir = parentDir === "." ? baseName : `${parentDir}/${baseName}`;
|
|
2133
|
+
const targetDir = normalizeToDbPath(args.targetDir ?? defaultTargetDir);
|
|
2134
|
+
const declarations = topLevelDeclarationsForPlan(content, ext);
|
|
2135
|
+
const grouped = new Map();
|
|
2136
|
+
for (const decl of declarations) {
|
|
2137
|
+
const key = grouped.size >= args.maxModules && !grouped.has(decl.suggested_module) ? "core" : decl.suggested_module;
|
|
2138
|
+
const list = grouped.get(key) ?? [];
|
|
2139
|
+
list.push(decl);
|
|
2140
|
+
grouped.set(key, list);
|
|
2141
|
+
}
|
|
2142
|
+
if (!grouped.size)
|
|
2143
|
+
grouped.set("core", []);
|
|
2144
|
+
const modules = Array.from(grouped.entries())
|
|
2145
|
+
.slice(0, args.maxModules)
|
|
2146
|
+
.map(([moduleName, decls]) => ({
|
|
2147
|
+
module: moduleName,
|
|
2148
|
+
target_path: targetPathForModule(args.filePath, targetDir, moduleName),
|
|
2149
|
+
declarations: decls.slice(0, 24).map((d) => `${d.kind} ${d.name} @L${d.line}`),
|
|
2150
|
+
reason: `Move whole ${moduleName}-related declarations together without changing behavior.`,
|
|
2151
|
+
}));
|
|
2152
|
+
return {
|
|
2153
|
+
ok: true,
|
|
2154
|
+
file_path: args.filePath,
|
|
2155
|
+
line_count: lines,
|
|
2156
|
+
bytes: st.size,
|
|
2157
|
+
huge_threshold_lines: DEVELOPMENT_HUGE_FILE_LINES,
|
|
2158
|
+
required_action: "mechanical_modularization",
|
|
2159
|
+
intent: args.intent,
|
|
2160
|
+
target_dir: targetDir,
|
|
2161
|
+
forbidden_patterns: ["*.generated.*", "*.parts", "*.rs.parts", "*_part*", "*Part*"],
|
|
2162
|
+
mechanical_rules: [
|
|
2163
|
+
"Move only complete declarations/impl blocks/functions/classes/types; do not split a declaration body.",
|
|
2164
|
+
"Use real module names and clear directory boundaries; do not create generated/parts/partN files.",
|
|
2165
|
+
"Preserve behavior, names, API semantics, data formats, side effects, and test expectations.",
|
|
2166
|
+
"Only add necessary module declarations, imports, pub(crate), and re-exports to make moved code compile.",
|
|
2167
|
+
"Run formatter, build/check, and relevant tests after each small phase.",
|
|
2168
|
+
],
|
|
2169
|
+
modules,
|
|
2170
|
+
steps: [
|
|
2171
|
+
"Create a dedicated mechanical modularization requirement before touching the huge file.",
|
|
2172
|
+
"Add the target module directory and move one cohesive declaration group at a time.",
|
|
2173
|
+
"Keep the original file as a thin entry/mod orchestration file where possible.",
|
|
2174
|
+
"After each group, run formatter and the smallest available compile/test command.",
|
|
2175
|
+
"Record the split with record_large_file_split(status='partial' or 'resolved').",
|
|
2176
|
+
"Resume the original feature only after the target huge file is no longer the default place for new code.",
|
|
2177
|
+
],
|
|
2178
|
+
validation: [
|
|
2179
|
+
"No *.generated.*, *.parts, *.rs.parts, or numbered part files were created.",
|
|
2180
|
+
"The original file line count decreased or contains only thin orchestration glue.",
|
|
2181
|
+
"Formatter passes.",
|
|
2182
|
+
"Build/check passes.",
|
|
2183
|
+
"Relevant tests pass.",
|
|
2184
|
+
],
|
|
2185
|
+
notes: declarations.length
|
|
2186
|
+
? [`Detected ${declarations.length} top-level declarations for mechanical grouping.`]
|
|
2187
|
+
: ["No top-level declarations were detected by lightweight scanning; split by obvious cohesive sections and verify after each move."],
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
1544
2190
|
function chunkTextByLines(content, opts) {
|
|
1545
2191
|
const lines = content.split(/\r?\n/);
|
|
1546
2192
|
if (lines.length === 0)
|
|
@@ -1744,12 +2390,42 @@ const StartRequirementArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
|
1744
2390
|
title: z.string().min(1),
|
|
1745
2391
|
background: z.string().optional().default(""),
|
|
1746
2392
|
close_previous: z.boolean().optional().default(true),
|
|
2393
|
+
scope_allow: z.array(z.string().min(1)).optional(),
|
|
2394
|
+
scope_deny: z.array(z.string().min(1)).optional(),
|
|
2395
|
+
allowed_paths: z.array(z.string().min(1)).optional(),
|
|
2396
|
+
denied_paths: z.array(z.string().min(1)).optional(),
|
|
1747
2397
|
}));
|
|
1748
2398
|
const SyncChangeIntentArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1749
2399
|
intent: z.string().min(1),
|
|
1750
2400
|
files: z.array(z.string().min(1)).optional(),
|
|
1751
2401
|
affected_files: z.array(z.string().min(1)).optional(),
|
|
1752
2402
|
}));
|
|
2403
|
+
const PreflightChangeScopeArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
|
|
2404
|
+
intent: z.string().optional().default(""),
|
|
2405
|
+
files: z.array(z.string().min(1)).optional(),
|
|
2406
|
+
planned_files: z.array(z.string().min(1)).optional(),
|
|
2407
|
+
change_mode: z
|
|
2408
|
+
.enum(["feature", "bugfix", "refactor", "mechanical_modularization", "emergency_hotfix"])
|
|
2409
|
+
.optional()
|
|
2410
|
+
.default("feature"),
|
|
2411
|
+
scope_allow: z.array(z.string().min(1)).optional(),
|
|
2412
|
+
scope_deny: z.array(z.string().min(1)).optional(),
|
|
2413
|
+
allowed_paths: z.array(z.string().min(1)).optional(),
|
|
2414
|
+
denied_paths: z.array(z.string().min(1)).optional(),
|
|
2415
|
+
}));
|
|
2416
|
+
const PlanLargeFileSplitArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
|
|
2417
|
+
file: z.string().min(1),
|
|
2418
|
+
intent: z.string().optional().default("mechanical modularization"),
|
|
2419
|
+
target_dir: z.string().optional(),
|
|
2420
|
+
max_modules: z.number().int().min(2).max(30).optional().default(12),
|
|
2421
|
+
}));
|
|
2422
|
+
const RecordLargeFileSplitArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
2423
|
+
file: z.string().min(1),
|
|
2424
|
+
status: z.enum(["planned", "in_progress", "partial", "resolved"]),
|
|
2425
|
+
summary: z.string().min(1),
|
|
2426
|
+
modules: z.array(z.string().min(1)).optional(),
|
|
2427
|
+
remaining_lines: z.number().int().min(0).optional(),
|
|
2428
|
+
}));
|
|
1753
2429
|
const QueryCodebaseArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
|
|
1754
2430
|
query: z.string().min(1),
|
|
1755
2431
|
}));
|
|
@@ -2079,6 +2755,7 @@ function compactGrepText(data) {
|
|
|
2079
2755
|
const lines = [
|
|
2080
2756
|
`grep ${data.backend}${fallback} mode=${data.mode} matches=${data.matches.length}/${total} truncated=${data.truncated}${candidateText} q="${oneLine(data.query, 100)}"`,
|
|
2081
2757
|
];
|
|
2758
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
|
|
2082
2759
|
if (data.ripgrep_error)
|
|
2083
2760
|
lines.push(`ripgrep_error ${oneLine(data.ripgrep_error, 180)}`);
|
|
2084
2761
|
for (const m of data.matches.slice(0, 80)) {
|
|
@@ -2108,15 +2785,18 @@ function compactReadTextFileText(data) {
|
|
|
2108
2785
|
const offset = data.offset != null ? ` offset=${data.offset}` : "";
|
|
2109
2786
|
const header = `file ${data.file_path}${offset} chars=${data.returned_chars}/${data.total_chars} truncated=${data.truncated}`;
|
|
2110
2787
|
const hint = data.truncated ? "\nhint: continue with offset or read_file_lines; use format=json for metadata fields" : "";
|
|
2111
|
-
|
|
2788
|
+
const warnings = compactDevelopmentWarningsText(data.development_warnings ?? []).join("\n");
|
|
2789
|
+
return `${header}${warnings ? `\n${warnings}` : ""}\n${data.text}${hint}`;
|
|
2112
2790
|
}
|
|
2113
2791
|
function compactReadFileLinesText(data) {
|
|
2114
2792
|
const header = `lines ${data.file_path}:${data.from_line}-${data.to_line} returned=${data.returned} truncated=${data.truncated}`;
|
|
2115
2793
|
const hint = data.truncated ? "\nhint: narrow range or raise max_lines/max_chars; use format=json for metadata fields" : "";
|
|
2116
|
-
|
|
2794
|
+
const warnings = compactDevelopmentWarningsText(data.development_warnings ?? []).join("\n");
|
|
2795
|
+
return `${header}${warnings ? `\n${warnings}` : ""}\n${data.text}${hint}`;
|
|
2117
2796
|
}
|
|
2118
2797
|
function compactQueryCodebaseText(data) {
|
|
2119
2798
|
const lines = [`query_codebase matches=${data.matches.length} q="${oneLine(data.query, 100)}"`];
|
|
2799
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
|
|
2120
2800
|
for (const m of data.matches.slice(0, 50)) {
|
|
2121
2801
|
lines.push(`${m.file_path}: ${m.type} ${m.name}${m.signature ? ` — ${oneLine(m.signature, 160)}` : ""}`);
|
|
2122
2802
|
}
|
|
@@ -2151,6 +2831,44 @@ function compactMaintenanceText(data) {
|
|
|
2151
2831
|
lines.push("hint: dry_run=false applies changes; vacuum=true reclaims sqlite file space after pruning");
|
|
2152
2832
|
return lines.join("\n");
|
|
2153
2833
|
}
|
|
2834
|
+
function compactPreflightChangeScopeText(data) {
|
|
2835
|
+
const req = data.active_requirement ? `#${data.active_requirement.id} ${data.active_requirement.title}` : "none";
|
|
2836
|
+
const lines = [
|
|
2837
|
+
`preflight_change_scope ok=${data.ok} safe_to_edit=${data.safe_to_edit} mode=${data.change_mode} requirement=${req} files=${data.files.length} intent="${oneLine(data.intent, 120)}"`,
|
|
2838
|
+
`action: ${oneLine(data.recommended_action, 180)}`,
|
|
2839
|
+
];
|
|
2840
|
+
if (data.required_action)
|
|
2841
|
+
lines.push(`required_action=${data.required_action}`);
|
|
2842
|
+
if (data.allowed_change_modes?.length)
|
|
2843
|
+
lines.push(`allowed_change_modes=${data.allowed_change_modes.join(",")}`);
|
|
2844
|
+
if (data.scope_contract) {
|
|
2845
|
+
lines.push(`scope allow_terms=${data.scope_contract.allow_terms.length} deny_terms=${data.scope_contract.deny_terms.length} allowed_paths=${data.scope_contract.allowed_paths.length} denied_paths=${data.scope_contract.denied_paths.length}`);
|
|
2846
|
+
}
|
|
2847
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings));
|
|
2848
|
+
if (!data.development_warnings.length)
|
|
2849
|
+
lines.push("- no development warnings");
|
|
2850
|
+
return lines.join("\n");
|
|
2851
|
+
}
|
|
2852
|
+
function compactLargeFileSplitPlanText(data) {
|
|
2853
|
+
const lines = [
|
|
2854
|
+
`large_file_split ok=${data.ok} file=${data.file_path} lines=${data.line_count} threshold=${data.huge_threshold_lines} action=${data.required_action}`,
|
|
2855
|
+
`target_dir=${data.target_dir}`,
|
|
2856
|
+
`forbidden=${data.forbidden_patterns.join(",")}`,
|
|
2857
|
+
];
|
|
2858
|
+
lines.push("modules:");
|
|
2859
|
+
for (const m of data.modules.slice(0, 20)) {
|
|
2860
|
+
const decls = m.declarations.length ? ` decls=${m.declarations.slice(0, 8).join("; ")}` : " decls=(manual sections)";
|
|
2861
|
+
lines.push(`- ${m.module} -> ${m.target_path}${decls}`);
|
|
2862
|
+
}
|
|
2863
|
+
lines.push("steps:");
|
|
2864
|
+
for (const step of data.steps.slice(0, 8))
|
|
2865
|
+
lines.push(`- ${step}`);
|
|
2866
|
+
lines.push("validation:");
|
|
2867
|
+
for (const v of data.validation.slice(0, 8))
|
|
2868
|
+
lines.push(`- ${v}`);
|
|
2869
|
+
lines.push("hint: use format=json for full declarations/rules");
|
|
2870
|
+
return lines.join("\n");
|
|
2871
|
+
}
|
|
2154
2872
|
function compactBootstrapText(data) {
|
|
2155
2873
|
const lines = [];
|
|
2156
2874
|
lines.push(`ok ctx ${data.root_source} watcher=${data.watcher_enabled ? (data.watcher_ready ? "ready" : "starting") : "off"} root=${data.project_root}`);
|
|
@@ -2175,6 +2893,7 @@ function compactBootstrapText(data) {
|
|
|
2175
2893
|
else {
|
|
2176
2894
|
lines.push("pending 0");
|
|
2177
2895
|
}
|
|
2896
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
|
|
2178
2897
|
if (data.items.length) {
|
|
2179
2898
|
lines.push("requirements:");
|
|
2180
2899
|
for (const item of data.items) {
|
|
@@ -4285,6 +5004,9 @@ function buildServerInstructions() {
|
|
|
4285
5004
|
"Built-in architecture and code-organization quality policy:",
|
|
4286
5005
|
BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
|
|
4287
5006
|
"",
|
|
5007
|
+
"Built-in requirement boundary and modularity quality policy:",
|
|
5008
|
+
BUILTIN_REQUIREMENT_BOUNDARY_AND_MODULARITY_INSTRUCTIONS,
|
|
5009
|
+
"",
|
|
4288
5010
|
"Built-in frontend output-purity quality policy:",
|
|
4289
5011
|
BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS,
|
|
4290
5012
|
"",
|
|
@@ -4315,7 +5037,13 @@ function buildServerInstructions() {
|
|
|
4315
5037
|
"- For raw repo text search with exact file+line+col matches, prefer grep({ query: <pattern> }). It uses ripgrep against real project files when available, applies built-in noise filters, and only falls back to indexed search if ripgrep is unavailable.",
|
|
4316
5038
|
"- To read a bounded segment of a file, prefer read_file_lines({ path: <file>, from_line/to_line or total_count }) over unbounded file reads.",
|
|
4317
5039
|
"- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
|
|
5040
|
+
" - When the user names a narrow feature/domain, pass scope_allow/allowed_paths and scope_deny/denied_paths when useful. These are generic project-specific scope boundaries, not business-specific built-ins.",
|
|
5041
|
+
"- BEFORE editing once target files/modules are known: call preflight_change_scope(intent, files/planned_files, optional scope_allow/scope_deny/allowed_paths/denied_paths). If ok=false/safe_to_edit=false or it returns development_warnings, stop before editing and narrow the plan unless the user explicitly expands the requirement.",
|
|
5042
|
+
"- Treat the active requirement as the only change boundary. Do not add extra business behavior, new flows, new fields, new interfaces, or touch completed/related features unless the user explicitly asked or the change is strictly necessary.",
|
|
5043
|
+
"- Do not keep piling new feature code into a large single file. Prefer small modules/services/components; if an implementation file is already large, split it before adding more responsibilities.",
|
|
5044
|
+
"- If preflight_change_scope/read_file_lines/grep/query_codebase/get_pending_changes/sync_change_intent returns huge_file_modularization_required, do not continue normal feature work. Call plan_large_file_split, perform mechanical modularization with real module names/directories, never create generated/parts/partN files, then call record_large_file_split before resuming normal feature work. Only use preflight_change_scope(change_mode='mechanical_modularization') for the split itself; use change_mode='emergency_hotfix' only for the smallest urgent fix and still record why the split was deferred.",
|
|
4318
5045
|
"- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
|
|
5046
|
+
"- If preflight_change_scope, read_file_lines, grep, query_codebase, get_pending_changes, or sync_change_intent returns development_warnings, address those warnings before continuing or explain why the current requirement truly needs that scope.",
|
|
4319
5047
|
"- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
|
|
4320
5048
|
"- When a requirement or user decision changes/reverses an older behavior, call upsert_decision(key, title, content, supersedes_req_ids?/supersedes_memory_ids?) and/or supersede_memory(...). Current decisions are shown in bootstrap_context/get_brain_dump and superseded memories are hidden from default semantic recall so stale requirements do not override newer facts.",
|
|
4321
5049
|
"- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
|
|
@@ -4974,14 +5702,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4974
5702
|
tools: [
|
|
4975
5703
|
{
|
|
4976
5704
|
name: "start_requirement",
|
|
4977
|
-
description: "MUST call BEFORE editing code. Starts/activates
|
|
5705
|
+
description: "MUST call BEFORE editing code. Starts/activates the concrete user requirement so subsequent changes stay inside that requirement boundary and do not accumulate unrelated work. Supports scope_allow/scope_deny and allowed_paths/denied_paths to prevent unrelated domain drift.",
|
|
4978
5706
|
inputSchema: toJsonSchemaCompat(StartRequirementArgsSchema),
|
|
4979
5707
|
},
|
|
4980
5708
|
{
|
|
4981
5709
|
name: "sync_change_intent",
|
|
4982
|
-
description: "MUST call AFTER you edit code and save files. Archives the intent summary
|
|
5710
|
+
description: "MUST call AFTER you edit code and save files. Archives the intent summary, links affected files to the current active requirement, and returns development_warnings for oversized files, broad change scope, or missing file targets.",
|
|
4983
5711
|
inputSchema: toJsonSchemaCompat(SyncChangeIntentArgsSchema),
|
|
4984
5712
|
},
|
|
5713
|
+
{
|
|
5714
|
+
name: "preflight_change_scope",
|
|
5715
|
+
description: "MUST call BEFORE editing once you know the intended files/modules. Checks planned files against the active requirement and optional generic scope_allow/scope_deny/allowed_paths/denied_paths. If ok=false/safe_to_edit=false, stop before editing and narrow the plan or scope contract. For huge files, use change_mode='mechanical_modularization' only when the task is to split the file.",
|
|
5716
|
+
inputSchema: toJsonSchemaCompat(PreflightChangeScopeArgsSchema),
|
|
5717
|
+
},
|
|
5718
|
+
{
|
|
5719
|
+
name: "plan_large_file_split",
|
|
5720
|
+
description: "Plan a mechanical modularization split for a huge implementation file. Produces real module names/directories and explicitly forbids generated/parts/partN files. Use this before normal feature work when preflight_change_scope returns huge_file_modularization_required.",
|
|
5721
|
+
inputSchema: toJsonSchemaCompat(PlanLargeFileSplitArgsSchema),
|
|
5722
|
+
},
|
|
5723
|
+
{
|
|
5724
|
+
name: "record_large_file_split",
|
|
5725
|
+
description: "Record the planned/in-progress/partial/resolved status of a huge-file mechanical modularization split so future sessions know the file is being decomposed and where modules moved.",
|
|
5726
|
+
inputSchema: toJsonSchemaCompat(RecordLargeFileSplitArgsSchema),
|
|
5727
|
+
},
|
|
4985
5728
|
{
|
|
4986
5729
|
name: "get_brain_dump",
|
|
4987
5730
|
description: "Restore recent requirements/changes/notes/summary/pending changes. Prefer bootstrap_context() at session start when you also want recall from the local memory store.",
|
|
@@ -4989,12 +5732,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4989
5732
|
},
|
|
4990
5733
|
{
|
|
4991
5734
|
name: "bootstrap_context",
|
|
4992
|
-
description: "MUST call at the start of every new chat/session. Returns brain dump + pending changes, and (if you pass query) matches from the local memory store to avoid guessing.",
|
|
5735
|
+
description: "MUST call at the start of every new chat/session. Returns brain dump + pending changes + development_warnings, and (if you pass query) matches from the local memory store to avoid guessing.",
|
|
4993
5736
|
inputSchema: toJsonSchemaCompat(BootstrapContextArgsSchema),
|
|
4994
5737
|
},
|
|
4995
5738
|
{
|
|
4996
5739
|
name: "get_pending_changes",
|
|
4997
|
-
description: "List files that changed locally but have not been acknowledged by sync_change_intent yet.
|
|
5740
|
+
description: "List files that changed locally but have not been acknowledged by sync_change_intent yet. Also returns development_warnings to catch god-file growth, broad change scope, and requirement-boundary drift.",
|
|
4998
5741
|
inputSchema: toJsonSchemaCompat(GetPendingChangesArgsSchema),
|
|
4999
5742
|
},
|
|
5000
5743
|
{
|
|
@@ -5039,7 +5782,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
5039
5782
|
},
|
|
5040
5783
|
{
|
|
5041
5784
|
name: "grep",
|
|
5042
|
-
description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable.",
|
|
5785
|
+
description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable. Returns development_warnings for cross-project paths or huge implementation-file matches.",
|
|
5043
5786
|
inputSchema: toJsonSchemaCompat(GrepArgsSchema),
|
|
5044
5787
|
},
|
|
5045
5788
|
{
|
|
@@ -5054,7 +5797,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
5054
5797
|
},
|
|
5055
5798
|
{
|
|
5056
5799
|
name: "read_file_lines",
|
|
5057
|
-
description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads.",
|
|
5800
|
+
description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads. Returns development_warnings when the target is a huge implementation file.",
|
|
5058
5801
|
inputSchema: toJsonSchemaCompat(ReadFileLinesArgsSchema),
|
|
5059
5802
|
},
|
|
5060
5803
|
{
|
|
@@ -5064,7 +5807,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
5064
5807
|
},
|
|
5065
5808
|
{
|
|
5066
5809
|
name: "query_codebase",
|
|
5067
|
-
description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find code
|
|
5810
|
+
description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find code; do not guess locations. Returns development_warnings when matches point at huge implementation files.",
|
|
5068
5811
|
inputSchema: toJsonSchemaCompat(QueryCodebaseArgsSchema),
|
|
5069
5812
|
},
|
|
5070
5813
|
{
|
|
@@ -5118,6 +5861,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5118
5861
|
if (toolName === "start_requirement") {
|
|
5119
5862
|
const args = StartRequirementArgsSchema.parse(rawArgs);
|
|
5120
5863
|
flushPendingChangeBuffer();
|
|
5864
|
+
const scope_contract = buildRequirementScopeContract({
|
|
5865
|
+
title: args.title,
|
|
5866
|
+
background: args.background,
|
|
5867
|
+
scope_allow: args.scope_allow,
|
|
5868
|
+
scope_deny: args.scope_deny,
|
|
5869
|
+
allowed_paths: args.allowed_paths,
|
|
5870
|
+
denied_paths: args.denied_paths,
|
|
5871
|
+
});
|
|
5872
|
+
const development_warnings = buildRequirementStartWarnings({
|
|
5873
|
+
title: args.title,
|
|
5874
|
+
background: args.background,
|
|
5875
|
+
close_previous: args.close_previous,
|
|
5876
|
+
});
|
|
5121
5877
|
if (args.close_previous) {
|
|
5122
5878
|
try {
|
|
5123
5879
|
completeAllActiveRequirementsStmt.run();
|
|
@@ -5131,13 +5887,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5131
5887
|
const id = Number(info.lastInsertRowid);
|
|
5132
5888
|
const background = args.background?.trim() ?? "";
|
|
5133
5889
|
const content = background ? `${args.title}\n\n${background}` : args.title;
|
|
5134
|
-
const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active" }), sha256Hex(content));
|
|
5890
|
+
const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active", scope_contract }), sha256Hex(content));
|
|
5135
5891
|
const memory_id = Number(memoryInfo.lastInsertRowid);
|
|
5136
5892
|
enqueueEmbedding(memory_id);
|
|
5137
5893
|
logActivity("start_requirement", {
|
|
5138
5894
|
req_id: id,
|
|
5139
5895
|
title: args.title,
|
|
5140
5896
|
closed_previous: args.close_previous,
|
|
5897
|
+
scope_contract,
|
|
5898
|
+
development_warnings: development_warnings.length,
|
|
5141
5899
|
});
|
|
5142
5900
|
return {
|
|
5143
5901
|
content: [
|
|
@@ -5148,6 +5906,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5148
5906
|
requirement: { id, title: args.title },
|
|
5149
5907
|
memory_item: { id: memory_id },
|
|
5150
5908
|
closed_previous: args.close_previous,
|
|
5909
|
+
scope_contract,
|
|
5910
|
+
development_warnings,
|
|
5151
5911
|
}),
|
|
5152
5912
|
},
|
|
5153
5913
|
],
|
|
@@ -5302,6 +6062,208 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5302
6062
|
],
|
|
5303
6063
|
};
|
|
5304
6064
|
}
|
|
6065
|
+
if (toolName === "preflight_change_scope") {
|
|
6066
|
+
const args = PreflightChangeScopeArgsSchema.parse(rawArgs);
|
|
6067
|
+
const changeMode = args.change_mode;
|
|
6068
|
+
flushPendingChangeBuffer();
|
|
6069
|
+
const files = (args.files ?? args.planned_files ?? []).filter((f) => typeof f === "string" && f.length > 0);
|
|
6070
|
+
const active = getActiveRequirementStmt.get();
|
|
6071
|
+
const explicitContract = buildRequirementScopeContract({
|
|
6072
|
+
title: active?.title ?? "",
|
|
6073
|
+
background: active?.context_data ?? "",
|
|
6074
|
+
scope_allow: args.scope_allow,
|
|
6075
|
+
scope_deny: args.scope_deny,
|
|
6076
|
+
allowed_paths: args.allowed_paths,
|
|
6077
|
+
denied_paths: args.denied_paths,
|
|
6078
|
+
});
|
|
6079
|
+
const fileInputs = files.map((file_path) => ({ file_path }));
|
|
6080
|
+
const development_warnings = [
|
|
6081
|
+
...buildDevelopmentWarnings(fileInputs, { includeUnspecified: fileInputs.length === 0 }),
|
|
6082
|
+
...buildScopeDriftWarnings({
|
|
6083
|
+
requirement: active,
|
|
6084
|
+
contract: explicitContract,
|
|
6085
|
+
intent: args.intent,
|
|
6086
|
+
files: fileInputs,
|
|
6087
|
+
includeMissingContractHint: true,
|
|
6088
|
+
}),
|
|
6089
|
+
];
|
|
6090
|
+
const scope_contract = mergeScopeContracts(active ? getRequirementScopeContract(active.id) : null, explicitContract);
|
|
6091
|
+
logActivity("preflight_change_scope", {
|
|
6092
|
+
req_id: active?.id ?? null,
|
|
6093
|
+
intent_preview: makePreviewText(args.intent, 200),
|
|
6094
|
+
change_mode: changeMode,
|
|
6095
|
+
files: files.slice(0, 25),
|
|
6096
|
+
files_total: files.length,
|
|
6097
|
+
development_warnings: development_warnings.length,
|
|
6098
|
+
});
|
|
6099
|
+
const hasTargetFiles = fileInputs.length > 0;
|
|
6100
|
+
const hugeWarnings = development_warnings.filter((w) => w.code === "huge_file_modularization_required");
|
|
6101
|
+
const hasHugeFile = hugeWarnings.length > 0;
|
|
6102
|
+
const blockingWarnings = development_warnings.filter((w) => isDevelopmentWarningBlockingForChangeMode(w, changeMode));
|
|
6103
|
+
const hasBlockingWarnings = blockingWarnings.length > 0;
|
|
6104
|
+
const safeToEdit = hasTargetFiles && !hasBlockingWarnings;
|
|
6105
|
+
const recommendedAction = !hasTargetFiles
|
|
6106
|
+
? "Identify the intended target files/modules and rerun preflight_change_scope before editing."
|
|
6107
|
+
: hasHugeFile && changeMode === "mechanical_modularization" && safeToEdit
|
|
6108
|
+
? "Proceed only with mechanical modularization: call plan_large_file_split, move whole declarations into real named modules/directories, avoid generated/parts files, validate, then record_large_file_split."
|
|
6109
|
+
: hasHugeFile && changeMode === "emergency_hotfix" && safeToEdit
|
|
6110
|
+
? "Proceed only with the smallest urgent fix, do not add new responsibilities, record why mechanical modularization was deferred, and plan/record the split next."
|
|
6111
|
+
: hasHugeFile
|
|
6112
|
+
? "Stop normal feature work. Call plan_large_file_split and perform mechanical modularization first, or rerun preflight_change_scope with change_mode='mechanical_modularization' for the split itself."
|
|
6113
|
+
: hasBlockingWarnings
|
|
6114
|
+
? "Stop before editing. Narrow the planned files or explicitly expand the current requirement/scope contract."
|
|
6115
|
+
: "Planned files are within the current generic scope checks.";
|
|
6116
|
+
const requiredAction = hasHugeFile && changeMode !== "mechanical_modularization"
|
|
6117
|
+
? "mechanical_modularization"
|
|
6118
|
+
: undefined;
|
|
6119
|
+
const allowedChangeModes = hasHugeFile
|
|
6120
|
+
? ["mechanical_modularization", "emergency_hotfix"]
|
|
6121
|
+
: undefined;
|
|
6122
|
+
const outputValue = {
|
|
6123
|
+
ok: safeToEdit,
|
|
6124
|
+
safe_to_edit: safeToEdit,
|
|
6125
|
+
change_mode: changeMode,
|
|
6126
|
+
recommended_action: recommendedAction,
|
|
6127
|
+
required_action: requiredAction,
|
|
6128
|
+
allowed_change_modes: allowedChangeModes,
|
|
6129
|
+
active_requirement: active ? { id: active.id, title: active.title } : null,
|
|
6130
|
+
intent: args.intent,
|
|
6131
|
+
files: files.map(normalizeToDbPath),
|
|
6132
|
+
scope_contract,
|
|
6133
|
+
development_warnings,
|
|
6134
|
+
};
|
|
6135
|
+
return {
|
|
6136
|
+
content: [
|
|
6137
|
+
{
|
|
6138
|
+
type: "text",
|
|
6139
|
+
text: toolCompactOrJson("preflight_change_scope", outputValue, compactPreflightChangeScopeText(outputValue), args.format),
|
|
6140
|
+
},
|
|
6141
|
+
],
|
|
6142
|
+
};
|
|
6143
|
+
}
|
|
6144
|
+
if (toolName === "plan_large_file_split") {
|
|
6145
|
+
const args = PlanLargeFileSplitArgsSchema.parse(rawArgs);
|
|
6146
|
+
flushPendingChangeBuffer();
|
|
6147
|
+
const resolved = resolveReadPathUnderProjectRoot(args.file);
|
|
6148
|
+
let stat;
|
|
6149
|
+
try {
|
|
6150
|
+
stat = fs.statSync(resolved.absPath);
|
|
6151
|
+
}
|
|
6152
|
+
catch (err) {
|
|
6153
|
+
return {
|
|
6154
|
+
isError: true,
|
|
6155
|
+
content: [{ type: "text", text: toolJson({ ok: false, error: `File not found: ${String(err)}` }) }],
|
|
6156
|
+
};
|
|
6157
|
+
}
|
|
6158
|
+
if (!stat.isFile()) {
|
|
6159
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: "Not a file" }) }] };
|
|
6160
|
+
}
|
|
6161
|
+
if (!isLikelySourceImplementationFile(resolved.dbFilePath)) {
|
|
6162
|
+
return {
|
|
6163
|
+
isError: true,
|
|
6164
|
+
content: [
|
|
6165
|
+
{
|
|
6166
|
+
type: "text",
|
|
6167
|
+
text: toolJson({
|
|
6168
|
+
ok: false,
|
|
6169
|
+
error: "Not a recognized source implementation file",
|
|
6170
|
+
file_path: resolved.dbFilePath,
|
|
6171
|
+
}),
|
|
6172
|
+
},
|
|
6173
|
+
],
|
|
6174
|
+
};
|
|
6175
|
+
}
|
|
6176
|
+
const lineInfo = countFileLinesBounded(resolved.absPath, 8_000_000);
|
|
6177
|
+
const lineCount = lineInfo?.lines ?? 0;
|
|
6178
|
+
if (lineCount < DEVELOPMENT_HUGE_FILE_LINES) {
|
|
6179
|
+
return {
|
|
6180
|
+
content: [
|
|
6181
|
+
{
|
|
6182
|
+
type: "text",
|
|
6183
|
+
text: toolJson({
|
|
6184
|
+
ok: false,
|
|
6185
|
+
file_path: resolved.dbFilePath,
|
|
6186
|
+
line_count: lineInfo?.truncated ? `${lineCount}+` : lineCount,
|
|
6187
|
+
huge_threshold_lines: DEVELOPMENT_HUGE_FILE_LINES,
|
|
6188
|
+
recommended_action: "This file is not above the huge-file threshold. Use normal focused modularity rules unless the user explicitly asked for refactoring.",
|
|
6189
|
+
}),
|
|
6190
|
+
},
|
|
6191
|
+
],
|
|
6192
|
+
};
|
|
6193
|
+
}
|
|
6194
|
+
let targetDir = args.target_dir;
|
|
6195
|
+
if (targetDir) {
|
|
6196
|
+
targetDir = resolveProjectPathUnderRoot(targetDir, { allowRoot: true }).dbFilePath;
|
|
6197
|
+
}
|
|
6198
|
+
const plan = buildLargeFileSplitPlan({
|
|
6199
|
+
filePath: resolved.dbFilePath,
|
|
6200
|
+
absPath: resolved.absPath,
|
|
6201
|
+
intent: args.intent,
|
|
6202
|
+
targetDir,
|
|
6203
|
+
maxModules: args.max_modules,
|
|
6204
|
+
});
|
|
6205
|
+
logActivity("plan_large_file_split", {
|
|
6206
|
+
file_path: plan.file_path,
|
|
6207
|
+
line_count: plan.line_count,
|
|
6208
|
+
target_dir: plan.target_dir,
|
|
6209
|
+
modules: plan.modules.map((m) => m.module),
|
|
6210
|
+
});
|
|
6211
|
+
return {
|
|
6212
|
+
content: [
|
|
6213
|
+
{
|
|
6214
|
+
type: "text",
|
|
6215
|
+
text: toolCompactOrJson("plan_large_file_split", plan, compactLargeFileSplitPlanText(plan), args.format),
|
|
6216
|
+
},
|
|
6217
|
+
],
|
|
6218
|
+
};
|
|
6219
|
+
}
|
|
6220
|
+
if (toolName === "record_large_file_split") {
|
|
6221
|
+
const args = RecordLargeFileSplitArgsSchema.parse(rawArgs);
|
|
6222
|
+
flushPendingChangeBuffer();
|
|
6223
|
+
const normalizedFile = normalizeToDbPath(args.file);
|
|
6224
|
+
const active = getActiveRequirementStmt.get();
|
|
6225
|
+
const modules = (args.modules ?? []).map(normalizeToDbPath);
|
|
6226
|
+
const content = [
|
|
6227
|
+
`Huge-file mechanical modularization ${args.status}: ${normalizedFile}`,
|
|
6228
|
+
"",
|
|
6229
|
+
args.summary,
|
|
6230
|
+
modules.length ? `\nModules:\n${modules.map((m) => `- ${m}`).join("\n")}` : "",
|
|
6231
|
+
args.remaining_lines != null ? `\nRemaining lines: ${args.remaining_lines}` : "",
|
|
6232
|
+
].filter(Boolean).join("\n");
|
|
6233
|
+
const meta = {
|
|
6234
|
+
tags: ["large-file-split", "mechanical-modularization"],
|
|
6235
|
+
file: normalizedFile,
|
|
6236
|
+
status: args.status,
|
|
6237
|
+
modules,
|
|
6238
|
+
remaining_lines: args.remaining_lines ?? null,
|
|
6239
|
+
active_requirement_id: active?.id ?? null,
|
|
6240
|
+
};
|
|
6241
|
+
const info = insertMemoryItemStmt.run("note", `large-file-split:${normalizedFile}:${args.status}`, content, normalizedFile, null, null, active?.id ?? null, safeJson(meta), sha256Hex(content));
|
|
6242
|
+
const id = Number(info.lastInsertRowid);
|
|
6243
|
+
enqueueEmbedding(id);
|
|
6244
|
+
logActivity("record_large_file_split", {
|
|
6245
|
+
memory_item_id: id,
|
|
6246
|
+
file_path: normalizedFile,
|
|
6247
|
+
status: args.status,
|
|
6248
|
+
modules: modules.slice(0, 20),
|
|
6249
|
+
remaining_lines: args.remaining_lines ?? null,
|
|
6250
|
+
});
|
|
6251
|
+
return {
|
|
6252
|
+
content: [
|
|
6253
|
+
{
|
|
6254
|
+
type: "text",
|
|
6255
|
+
text: toolJson({
|
|
6256
|
+
ok: true,
|
|
6257
|
+
note: { id },
|
|
6258
|
+
file_path: normalizedFile,
|
|
6259
|
+
status: args.status,
|
|
6260
|
+
modules,
|
|
6261
|
+
remaining_lines: args.remaining_lines ?? null,
|
|
6262
|
+
}),
|
|
6263
|
+
},
|
|
6264
|
+
],
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
5305
6267
|
if (toolName === "sync_change_intent") {
|
|
5306
6268
|
const args = SyncChangeIntentArgsSchema.parse(rawArgs);
|
|
5307
6269
|
flushPendingChangeBuffer();
|
|
@@ -5386,12 +6348,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5386
6348
|
}
|
|
5387
6349
|
});
|
|
5388
6350
|
insertTx();
|
|
6351
|
+
const development_warnings = [
|
|
6352
|
+
...buildDevelopmentWarnings(synced_files, {
|
|
6353
|
+
includeUnspecified: synced_files.some((f) => f.file_path === "(unspecified)"),
|
|
6354
|
+
}),
|
|
6355
|
+
...buildScopeDriftWarnings({
|
|
6356
|
+
requirement: active,
|
|
6357
|
+
intent: args.intent,
|
|
6358
|
+
files: synced_files,
|
|
6359
|
+
}),
|
|
6360
|
+
];
|
|
5389
6361
|
logActivity("sync_change_intent", {
|
|
5390
6362
|
req_id: active.id,
|
|
5391
6363
|
title: active.title,
|
|
5392
6364
|
intent_preview: makePreviewText(args.intent, 200),
|
|
5393
6365
|
files: synced_files.slice(0, 25),
|
|
5394
6366
|
files_total: synced_files.length,
|
|
6367
|
+
development_warnings: development_warnings.length,
|
|
5395
6368
|
});
|
|
5396
6369
|
return {
|
|
5397
6370
|
content: [
|
|
@@ -5402,6 +6375,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5402
6375
|
linked_to_requirement: { id: active.id, title: active.title },
|
|
5403
6376
|
synced_files,
|
|
5404
6377
|
created,
|
|
6378
|
+
development_warnings,
|
|
5405
6379
|
}),
|
|
5406
6380
|
},
|
|
5407
6381
|
],
|
|
@@ -5442,6 +6416,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5442
6416
|
const pending_total = mergedPending.total;
|
|
5443
6417
|
const pending_truncated = mergedPending.truncated;
|
|
5444
6418
|
const pending_changes = mergedPending.page;
|
|
6419
|
+
const activeForScope = getActiveRequirementStmt.get();
|
|
6420
|
+
const development_warnings = [
|
|
6421
|
+
...buildDevelopmentWarnings(pending_changes),
|
|
6422
|
+
...(activeForScope
|
|
6423
|
+
? buildScopeDriftWarnings({ requirement: activeForScope, files: pending_changes })
|
|
6424
|
+
: []),
|
|
6425
|
+
];
|
|
5445
6426
|
const q = args.query?.trim() ?? "";
|
|
5446
6427
|
const semanticKinds = args.kinds?.length ? args.kinds : BOOTSTRAP_DEFAULT_CONTEXT_KINDS;
|
|
5447
6428
|
const semantic = q
|
|
@@ -5506,6 +6487,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5506
6487
|
pending_limit,
|
|
5507
6488
|
pending_truncated,
|
|
5508
6489
|
pending_changes,
|
|
6490
|
+
development_warnings,
|
|
5509
6491
|
items,
|
|
5510
6492
|
semantic,
|
|
5511
6493
|
};
|
|
@@ -5553,6 +6535,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5553
6535
|
const pending_total = mergedPending.total;
|
|
5554
6536
|
const pending_truncated = mergedPending.truncated;
|
|
5555
6537
|
const pending_changes = mergedPending.page;
|
|
6538
|
+
const activeForScope = getActiveRequirementStmt.get();
|
|
6539
|
+
const development_warnings = [
|
|
6540
|
+
...buildDevelopmentWarnings(pending_changes),
|
|
6541
|
+
...(activeForScope
|
|
6542
|
+
? buildScopeDriftWarnings({ requirement: activeForScope, files: pending_changes })
|
|
6543
|
+
: []),
|
|
6544
|
+
];
|
|
5556
6545
|
logActivity("get_brain_dump", {
|
|
5557
6546
|
pending_total,
|
|
5558
6547
|
pending_returned: pending_changes.length,
|
|
@@ -5597,6 +6586,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5597
6586
|
pending_limit,
|
|
5598
6587
|
pending_truncated,
|
|
5599
6588
|
pending_changes,
|
|
6589
|
+
development_warnings,
|
|
5600
6590
|
items,
|
|
5601
6591
|
semantic: null,
|
|
5602
6592
|
};
|
|
@@ -5619,18 +6609,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5619
6609
|
const total = mergedPending.total;
|
|
5620
6610
|
const truncated = mergedPending.truncated;
|
|
5621
6611
|
const pending = mergedPending.page;
|
|
6612
|
+
const activeForScope = getActiveRequirementStmt.get();
|
|
6613
|
+
const development_warnings = [
|
|
6614
|
+
...buildDevelopmentWarnings(pending),
|
|
6615
|
+
...(activeForScope ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending }) : []),
|
|
6616
|
+
];
|
|
5622
6617
|
logActivity("get_pending_changes", {
|
|
5623
6618
|
total,
|
|
5624
6619
|
offset,
|
|
5625
6620
|
limit,
|
|
5626
6621
|
returned: pending.length,
|
|
5627
6622
|
truncated,
|
|
6623
|
+
development_warnings: development_warnings.length,
|
|
5628
6624
|
});
|
|
5629
6625
|
return {
|
|
5630
6626
|
content: [
|
|
5631
6627
|
{
|
|
5632
6628
|
type: "text",
|
|
5633
|
-
text: toolJson({ ok: true, total, offset, limit, truncated, pending }),
|
|
6629
|
+
text: toolJson({ ok: true, total, offset, limit, truncated, pending, development_warnings }),
|
|
5634
6630
|
},
|
|
5635
6631
|
],
|
|
5636
6632
|
};
|
|
@@ -5821,6 +6817,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5821
6817
|
const includePaths = args.include_paths?.length ? args.include_paths : null;
|
|
5822
6818
|
const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
|
|
5823
6819
|
const maxResults = args.max_results;
|
|
6820
|
+
const development_warnings = [
|
|
6821
|
+
...buildCrossProjectPathWarnings(includePaths),
|
|
6822
|
+
...buildCrossProjectPathWarnings(excludePaths),
|
|
6823
|
+
];
|
|
5824
6824
|
const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
|
|
5825
6825
|
const ripgrepResult = runRipgrepSearch({
|
|
5826
6826
|
query: q,
|
|
@@ -5832,6 +6832,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5832
6832
|
maxResults,
|
|
5833
6833
|
});
|
|
5834
6834
|
if (ripgrepResult.ok) {
|
|
6835
|
+
const grepDevelopmentWarnings = [
|
|
6836
|
+
...development_warnings,
|
|
6837
|
+
...buildMatchedFileDevelopmentWarnings(ripgrepResult.matches.map((m) => m.file_path)),
|
|
6838
|
+
];
|
|
5835
6839
|
logActivity("grep", {
|
|
5836
6840
|
backend: ripgrepResult.backend,
|
|
5837
6841
|
rg_command: ripgrepResult.rg_command,
|
|
@@ -5844,6 +6848,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5844
6848
|
matches: ripgrepResult.matches.length,
|
|
5845
6849
|
total_matches: ripgrepResult.total_matches,
|
|
5846
6850
|
truncated: ripgrepResult.truncated,
|
|
6851
|
+
development_warnings: grepDevelopmentWarnings.length,
|
|
5847
6852
|
});
|
|
5848
6853
|
const outputValue = {
|
|
5849
6854
|
ok: true,
|
|
@@ -5858,6 +6863,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5858
6863
|
matches: ripgrepResult.matches,
|
|
5859
6864
|
total_matches: ripgrepResult.total_matches,
|
|
5860
6865
|
truncated: ripgrepResult.truncated,
|
|
6866
|
+
development_warnings: grepDevelopmentWarnings,
|
|
5861
6867
|
};
|
|
5862
6868
|
return {
|
|
5863
6869
|
content: [
|
|
@@ -5923,6 +6929,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5923
6929
|
],
|
|
5924
6930
|
};
|
|
5925
6931
|
}
|
|
6932
|
+
const grepDevelopmentWarnings = [
|
|
6933
|
+
...development_warnings,
|
|
6934
|
+
...buildMatchedFileDevelopmentWarnings(indexedResult.matches.map((m) => m.file_path)),
|
|
6935
|
+
];
|
|
5926
6936
|
logActivity("grep", {
|
|
5927
6937
|
backend: indexedResult.backend,
|
|
5928
6938
|
fallback_reason: "ripgrep_unavailable",
|
|
@@ -5939,6 +6949,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5939
6949
|
candidates_scanned: indexedResult.candidates.scanned,
|
|
5940
6950
|
matches: indexedResult.matches.length,
|
|
5941
6951
|
truncated: indexedResult.truncated,
|
|
6952
|
+
development_warnings: grepDevelopmentWarnings.length,
|
|
5942
6953
|
});
|
|
5943
6954
|
const outputValue = {
|
|
5944
6955
|
ok: true,
|
|
@@ -5957,6 +6968,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5957
6968
|
candidates: indexedResult.candidates,
|
|
5958
6969
|
matches: indexedResult.matches,
|
|
5959
6970
|
truncated: indexedResult.truncated,
|
|
6971
|
+
development_warnings: grepDevelopmentWarnings,
|
|
5960
6972
|
};
|
|
5961
6973
|
return {
|
|
5962
6974
|
content: [
|
|
@@ -6076,6 +7088,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6076
7088
|
total_chars: result.totalChars,
|
|
6077
7089
|
truncated: result.truncated,
|
|
6078
7090
|
});
|
|
7091
|
+
const development_warnings = buildFileReadDevelopmentWarnings(resolved.dbFilePath, resolved.absPath, st);
|
|
6079
7092
|
const outputValue = {
|
|
6080
7093
|
ok: true,
|
|
6081
7094
|
file_path: resolved.dbFilePath,
|
|
@@ -6083,6 +7096,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6083
7096
|
returned_chars: result.returnedChars,
|
|
6084
7097
|
total_chars: result.totalChars,
|
|
6085
7098
|
truncated: result.truncated,
|
|
7099
|
+
development_warnings,
|
|
6086
7100
|
text: result.text,
|
|
6087
7101
|
};
|
|
6088
7102
|
return {
|
|
@@ -6212,6 +7226,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6212
7226
|
returned: result.returned,
|
|
6213
7227
|
truncated: result.truncated,
|
|
6214
7228
|
});
|
|
7229
|
+
const development_warnings = buildFileReadDevelopmentWarnings(resolved.dbFilePath, resolved.absPath, st);
|
|
6215
7230
|
const outputValue = {
|
|
6216
7231
|
ok: true,
|
|
6217
7232
|
file_path: resolved.dbFilePath,
|
|
@@ -6219,6 +7234,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6219
7234
|
to_line: toLine,
|
|
6220
7235
|
returned: result.returned,
|
|
6221
7236
|
truncated: result.truncated,
|
|
7237
|
+
development_warnings,
|
|
6222
7238
|
text: result.text,
|
|
6223
7239
|
};
|
|
6224
7240
|
return {
|
|
@@ -6237,12 +7253,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6237
7253
|
const like = `%${escaped}%`;
|
|
6238
7254
|
const rows = searchSymbolsStmt.all(like, like, q, like, 250);
|
|
6239
7255
|
const filtered = rows.filter((r) => !shouldIgnoreDbFilePath(r.file_path)).slice(0, 50);
|
|
7256
|
+
const development_warnings = buildMatchedFileDevelopmentWarnings(filtered.map((m) => m.file_path));
|
|
6240
7257
|
logActivity("query_codebase", {
|
|
6241
7258
|
query: q,
|
|
6242
7259
|
matches: filtered.length,
|
|
7260
|
+
development_warnings: development_warnings.length,
|
|
6243
7261
|
sample: filtered.slice(0, 10).map((m) => ({ name: m.name, type: m.type, file_path: m.file_path })),
|
|
6244
7262
|
});
|
|
6245
|
-
const outputValue = { ok: true, query: q, matches: filtered };
|
|
7263
|
+
const outputValue = { ok: true, query: q, matches: filtered, development_warnings };
|
|
6246
7264
|
return {
|
|
6247
7265
|
content: [
|
|
6248
7266
|
{
|