@agentbridge1/cli 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +4 -4
- package/dist/commands/start.js +1 -0
- package/dist/commands/watch.js +3 -2
- package/dist/contract-intelligence.js +597 -0
- package/dist/contract-verdict.js +79 -26
- package/dist/diff-reader.js +200 -0
- package/dist/intent-parser.js +178 -0
- package/dist/mcp/agentbridge-mcp.js +585 -27
- package/dist/mcp/agentbridge-mcp.js.map +4 -4
- package/dist/proof-parser.js +118 -0
- package/dist/session-state.js +15 -0
- package/dist/session.js +10 -0
- package/package.json +1 -1
package/dist/contract-verdict.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.detectOutOfScopeFiles = detectOutOfScopeFiles;
|
|
4
4
|
exports.renderContractVerdict = renderContractVerdict;
|
|
5
5
|
const domain_resolution_1 = require("./domain-resolution");
|
|
6
|
+
const contract_intelligence_1 = require("./contract-intelligence");
|
|
7
|
+
const diff_reader_1 = require("./diff-reader");
|
|
6
8
|
const local_proof_1 = require("./local-proof");
|
|
7
9
|
const INTENT_STOP_WORDS = new Set([
|
|
8
10
|
"a",
|
|
@@ -115,34 +117,39 @@ function renderContractVerdict(session, changedFiles, proofRun, domains = []) {
|
|
|
115
117
|
.map(normalizePath)
|
|
116
118
|
.filter((file) => file.length > 0 && !(0, local_proof_1.isProofNoiseFile)(file));
|
|
117
119
|
const outOfScope = detectOutOfScopeFiles(meaningfulFiles, session, domains);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
120
|
+
const contract = session?.intent?.trim()
|
|
121
|
+
? (0, contract_intelligence_1.localSessionToContractIntelligence)(session)
|
|
122
|
+
: null;
|
|
123
|
+
// Read diff once — fresh from git, not stored in session
|
|
124
|
+
const diff = meaningfulFiles.length > 0 ? (0, diff_reader_1.readRepoDiff)(meaningfulFiles) : null;
|
|
125
|
+
// Evaluate criteria if we have a session with intent
|
|
126
|
+
const criteriaEvals = contract
|
|
127
|
+
? (0, contract_intelligence_1.evaluateCompletionCriteria)({
|
|
128
|
+
contract,
|
|
129
|
+
changedFiles: meaningfulFiles,
|
|
130
|
+
proofRun,
|
|
131
|
+
diff,
|
|
132
|
+
})
|
|
133
|
+
: null;
|
|
134
|
+
const proofNeeded = contract?.proofNeeded ?? [];
|
|
135
|
+
const evidenced = criteriaEvals?.filter((e) => e.status === "evidence_found") ?? [];
|
|
136
|
+
// Next step: criteria-driven if available, else generic
|
|
137
|
+
let nextStep;
|
|
138
|
+
if (criteriaEvals && criteriaEvals.length > 0) {
|
|
139
|
+
nextStep = (0, contract_intelligence_1.nextStepFromCriteria)(criteriaEvals, proofNeeded);
|
|
140
|
+
}
|
|
141
|
+
else if (!proofRun) {
|
|
137
142
|
nextStep = "Run the relevant test command so AgentBridge can record proof for this contract.";
|
|
138
143
|
}
|
|
139
144
|
else if (outOfScope.length > 0) {
|
|
140
|
-
nextStep =
|
|
141
|
-
"Ask the agent to justify the out-of-scope files, or revert them before accepting the work.";
|
|
145
|
+
nextStep = "Ask the agent to justify the out-of-scope files, or revert them before accepting the work.";
|
|
142
146
|
}
|
|
143
147
|
else if (meaningfulFiles.length === 0) {
|
|
144
148
|
nextStep = "No work was recorded. Start a new contract when you are ready to implement.";
|
|
145
149
|
}
|
|
150
|
+
else {
|
|
151
|
+
nextStep = "Contract looks complete. Commit when ready.";
|
|
152
|
+
}
|
|
146
153
|
const lines = [
|
|
147
154
|
"AgentBridge Contract Verdict",
|
|
148
155
|
"─────────────────────────────────────────",
|
|
@@ -160,13 +167,59 @@ function renderContractVerdict(session, changedFiles, proofRun, domains = []) {
|
|
|
160
167
|
lines.push(` - ${file}`);
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
if (criteriaEvals && criteriaEvals.length > 0) {
|
|
171
|
+
lines.push("", "Here's what \"done\" needed to prove:");
|
|
172
|
+
for (const e of criteriaEvals) {
|
|
173
|
+
const icon = e.status === "evidence_found"
|
|
174
|
+
? "✓"
|
|
175
|
+
: e.status === "partial_evidence"
|
|
176
|
+
? "~"
|
|
177
|
+
: e.status === "cannot_verify"
|
|
178
|
+
? "?"
|
|
179
|
+
: "✗";
|
|
180
|
+
lines.push(` ${icon} [${e.status}] ${e.criterion}`);
|
|
181
|
+
lines.push(` ${e.evidence}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push("", "Here's what appears missing:");
|
|
186
|
+
const missing = [];
|
|
187
|
+
if (!session?.intent?.trim())
|
|
188
|
+
missing.push("No contract intent was recorded.");
|
|
189
|
+
if (meaningfulFiles.length === 0)
|
|
190
|
+
missing.push("No files changed since the contract baseline.");
|
|
191
|
+
if (!proofRun)
|
|
192
|
+
missing.push("No proof/test run was recorded.");
|
|
193
|
+
else if (proofRun.status === "failed")
|
|
194
|
+
missing.push(`Proof failed: ${proofRun.command} (exit ${proofRun.exitCode}).`);
|
|
195
|
+
if (missing.length === 0) {
|
|
196
|
+
lines.push(" Nothing obvious from this contract check.");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
for (const item of missing)
|
|
200
|
+
lines.push(` - ${item}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (evidenced.length > 0 && criteriaEvals) {
|
|
204
|
+
lines.push("", "Here's what AgentBridge saw evidence for:");
|
|
205
|
+
for (const e of evidenced) {
|
|
206
|
+
lines.push(` - ${e.criterion}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const stillUnproven = criteriaEvals
|
|
210
|
+
? criteriaEvals.filter((e) => e.status === "no_evidence")
|
|
211
|
+
: [];
|
|
212
|
+
lines.push("", "Here's what is still unproven:");
|
|
213
|
+
if (stillUnproven.length === 0 && (!criteriaEvals || criteriaEvals.length === 0)) {
|
|
214
|
+
lines.push(!proofRun ? " No proof/test run was recorded." : " Nothing obvious from this contract check.");
|
|
215
|
+
}
|
|
216
|
+
else if (stillUnproven.length === 0) {
|
|
217
|
+
lines.push(" Nothing detected as fully unproven.");
|
|
166
218
|
}
|
|
167
219
|
else {
|
|
168
|
-
for (const
|
|
169
|
-
lines.push(` - ${
|
|
220
|
+
for (const e of stillUnproven) {
|
|
221
|
+
lines.push(` - ${e.criterion}`);
|
|
222
|
+
lines.push(` ${e.evidence}`);
|
|
170
223
|
}
|
|
171
224
|
}
|
|
172
225
|
lines.push("", "Here's where the work drifted out of scope:");
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Diff reader — parses `git diff HEAD` output into structured change data.
|
|
4
|
+
* All logic is deterministic regex on git unified diff format. No LLM.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.readRepoDiff = readRepoDiff;
|
|
8
|
+
exports.parseDiffString = parseDiffString;
|
|
9
|
+
exports.getDiffForFile = getDiffForFile;
|
|
10
|
+
exports.summariseDiff = summariseDiff;
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
// Function declarations
|
|
13
|
+
const FN_ADDED_RE = /^\+\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
|
|
14
|
+
const FN_REMOVED_RE = /^-\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
|
|
15
|
+
// Arrow / const functions: `const foo = (` or `const foo = async (`
|
|
16
|
+
const ARROW_ADDED_RE = /^\+\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(/;
|
|
17
|
+
const ARROW_REMOVED_RE = /^-\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(/;
|
|
18
|
+
// Non-function consts: `const FOO_BAR =` (all-caps or mixed without open paren)
|
|
19
|
+
const CONST_ADDED_RE = /^\+\s*(?:export\s+)?const\s+(\w+)\s*=/;
|
|
20
|
+
const CONST_REMOVED_RE = /^-\s*(?:export\s+)?const\s+(\w+)\s*=/;
|
|
21
|
+
// Import lines
|
|
22
|
+
const IMPORT_ADDED_RE = /^\+\s*import\s+.+\s+from\s+['"]([^'"]+)['"]/;
|
|
23
|
+
const IMPORT_REMOVED_RE = /^-\s*import\s+.+\s+from\s+['"]([^'"]+)['"]/;
|
|
24
|
+
// Named exports (excluding consts/functions already captured)
|
|
25
|
+
const EXPORT_ADDED_RE = /^\+\s*export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/;
|
|
26
|
+
const EXPORT_REMOVED_RE = /^-\s*export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/;
|
|
27
|
+
// Guard clauses: if(!x) throw / return / continue / break
|
|
28
|
+
const GUARD_RE = /^\+.*\bif\s*\([^)]*\).*\b(?:throw|return new Error|return null|return undefined)\b/;
|
|
29
|
+
// File header in git diff: "diff --git a/foo b/foo"
|
|
30
|
+
const FILE_HEADER_RE = /^diff --git a\/(.+) b\/.+/;
|
|
31
|
+
// Hunk header: @@ -a,b +c,d @@
|
|
32
|
+
const HUNK_RE = /^@@/;
|
|
33
|
+
function isArrowOrFnConst(line, added) {
|
|
34
|
+
const re = added ? ARROW_ADDED_RE : ARROW_REMOVED_RE;
|
|
35
|
+
return re.exec(line)?.[1] ?? null;
|
|
36
|
+
}
|
|
37
|
+
function isRegularConst(line, added) {
|
|
38
|
+
// Only count if it's NOT an arrow function (no open paren immediately after =)
|
|
39
|
+
const arrowRe = added ? ARROW_ADDED_RE : ARROW_REMOVED_RE;
|
|
40
|
+
if (arrowRe.test(line))
|
|
41
|
+
return null;
|
|
42
|
+
const re = added ? CONST_ADDED_RE : CONST_REMOVED_RE;
|
|
43
|
+
return re.exec(line)?.[1] ?? null;
|
|
44
|
+
}
|
|
45
|
+
function parseFileDiff(fileName, diffLines) {
|
|
46
|
+
const fd = {
|
|
47
|
+
file: fileName,
|
|
48
|
+
functions_added: [],
|
|
49
|
+
functions_removed: [],
|
|
50
|
+
functions_changed: [],
|
|
51
|
+
constants_added: [],
|
|
52
|
+
constants_removed: [],
|
|
53
|
+
imports_added: [],
|
|
54
|
+
imports_removed: [],
|
|
55
|
+
exports_added: [],
|
|
56
|
+
exports_removed: [],
|
|
57
|
+
guard_clauses_added: 0,
|
|
58
|
+
lines_added: 0,
|
|
59
|
+
lines_removed: 0,
|
|
60
|
+
};
|
|
61
|
+
for (const line of diffLines) {
|
|
62
|
+
if (HUNK_RE.test(line))
|
|
63
|
+
continue;
|
|
64
|
+
const isAdd = line.startsWith("+") && !line.startsWith("+++");
|
|
65
|
+
const isDel = line.startsWith("-") && !line.startsWith("---");
|
|
66
|
+
if (isAdd)
|
|
67
|
+
fd.lines_added++;
|
|
68
|
+
if (isDel)
|
|
69
|
+
fd.lines_removed++;
|
|
70
|
+
// Functions
|
|
71
|
+
const fnAdded = FN_ADDED_RE.exec(line)?.[1] ?? isArrowOrFnConst(line, true);
|
|
72
|
+
if (fnAdded) {
|
|
73
|
+
if (fd.functions_removed.includes(fnAdded)) {
|
|
74
|
+
fd.functions_changed.push(fnAdded);
|
|
75
|
+
fd.functions_removed.splice(fd.functions_removed.indexOf(fnAdded), 1);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
fd.functions_added.push(fnAdded);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const fnRemoved = FN_REMOVED_RE.exec(line)?.[1] ?? isArrowOrFnConst(line, false);
|
|
82
|
+
if (fnRemoved) {
|
|
83
|
+
const addedIdx = fd.functions_added.indexOf(fnRemoved);
|
|
84
|
+
if (addedIdx >= 0) {
|
|
85
|
+
fd.functions_changed.push(fnRemoved);
|
|
86
|
+
fd.functions_added.splice(addedIdx, 1);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fd.functions_removed.push(fnRemoved);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Constants (non-function)
|
|
93
|
+
const constAdded = isRegularConst(line, true);
|
|
94
|
+
if (constAdded)
|
|
95
|
+
fd.constants_added.push(constAdded);
|
|
96
|
+
const constRemoved = isRegularConst(line, false);
|
|
97
|
+
if (constRemoved)
|
|
98
|
+
fd.constants_removed.push(constRemoved);
|
|
99
|
+
// Imports
|
|
100
|
+
const importAdded = IMPORT_ADDED_RE.exec(line)?.[1];
|
|
101
|
+
if (importAdded)
|
|
102
|
+
fd.imports_added.push(importAdded);
|
|
103
|
+
const importRemoved = IMPORT_REMOVED_RE.exec(line)?.[1];
|
|
104
|
+
if (importRemoved)
|
|
105
|
+
fd.imports_removed.push(importRemoved);
|
|
106
|
+
// Exports
|
|
107
|
+
const exportAdded = EXPORT_ADDED_RE.exec(line)?.[1];
|
|
108
|
+
if (exportAdded && !fd.functions_added.includes(exportAdded) && !fd.constants_added.includes(exportAdded)) {
|
|
109
|
+
fd.exports_added.push(exportAdded);
|
|
110
|
+
}
|
|
111
|
+
const exportRemoved = EXPORT_REMOVED_RE.exec(line)?.[1];
|
|
112
|
+
if (exportRemoved && !fd.functions_removed.includes(exportRemoved) && !fd.constants_removed.includes(exportRemoved)) {
|
|
113
|
+
fd.exports_removed.push(exportRemoved);
|
|
114
|
+
}
|
|
115
|
+
// Guard clauses
|
|
116
|
+
if (isAdd && GUARD_RE.test(line))
|
|
117
|
+
fd.guard_clauses_added++;
|
|
118
|
+
}
|
|
119
|
+
return fd;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reads git diff HEAD for the given changed files and returns structured diffs.
|
|
123
|
+
* Falls back gracefully if git is unavailable or diff fails.
|
|
124
|
+
*/
|
|
125
|
+
function readRepoDiff(changedFiles) {
|
|
126
|
+
if (changedFiles.length === 0)
|
|
127
|
+
return { files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
|
|
128
|
+
let rawDiff;
|
|
129
|
+
try {
|
|
130
|
+
rawDiff = (0, node_child_process_1.execFileSync)("git", ["diff", "HEAD", "--unified=0", "--", ...changedFiles], {
|
|
131
|
+
encoding: "utf-8",
|
|
132
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
133
|
+
timeout: 5000,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Fallback: try diff against staged + unstaged
|
|
138
|
+
try {
|
|
139
|
+
rawDiff = (0, node_child_process_1.execFileSync)("git", ["diff", "--unified=0", "--", ...changedFiles], {
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return parseDiffString(rawDiff);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Parse a raw git diff string into RepoDiff. Exported for testing without git.
|
|
153
|
+
*/
|
|
154
|
+
function parseDiffString(rawDiff) {
|
|
155
|
+
const lines = rawDiff.split("\n");
|
|
156
|
+
const fileBlocks = [];
|
|
157
|
+
let current = null;
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
const fileMatch = FILE_HEADER_RE.exec(line);
|
|
160
|
+
if (fileMatch) {
|
|
161
|
+
if (current)
|
|
162
|
+
fileBlocks.push(current);
|
|
163
|
+
current = { name: fileMatch[1], lines: [] };
|
|
164
|
+
}
|
|
165
|
+
else if (current) {
|
|
166
|
+
current.lines.push(line);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (current)
|
|
170
|
+
fileBlocks.push(current);
|
|
171
|
+
const files = fileBlocks.map((b) => parseFileDiff(b.name, b.lines));
|
|
172
|
+
return {
|
|
173
|
+
files,
|
|
174
|
+
totalLinesAdded: files.reduce((s, f) => s + f.lines_added, 0),
|
|
175
|
+
totalLinesRemoved: files.reduce((s, f) => s + f.lines_removed, 0),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Returns a FileDiff for a specific file, or null if not in the diff.
|
|
180
|
+
*/
|
|
181
|
+
function getDiffForFile(diff, file) {
|
|
182
|
+
const normalized = file.replaceAll("\\", "/");
|
|
183
|
+
return diff.files.find((f) => f.file === normalized || f.file.endsWith(`/${normalized}`) || normalized.endsWith(`/${f.file}`)) ?? null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Summarise diff changes across all files in a few readable tokens for evidence strings.
|
|
187
|
+
*/
|
|
188
|
+
function summariseDiff(diff) {
|
|
189
|
+
const allExportsAdded = diff.files.flatMap((f) => f.exports_added.concat(f.functions_added));
|
|
190
|
+
const allGuards = diff.files.reduce((s, f) => s + f.guard_clauses_added, 0);
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (diff.totalLinesAdded > 0 || diff.totalLinesRemoved > 0) {
|
|
193
|
+
parts.push(`+${diff.totalLinesAdded}/-${diff.totalLinesRemoved} lines`);
|
|
194
|
+
}
|
|
195
|
+
if (allExportsAdded.length > 0)
|
|
196
|
+
parts.push(`${allExportsAdded.length} new export(s): ${allExportsAdded.slice(0, 3).join(", ")}`);
|
|
197
|
+
if (allGuards > 0)
|
|
198
|
+
parts.push(`${allGuards} guard clause(s) added`);
|
|
199
|
+
return parts.join("; ");
|
|
200
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Intent parser — extracts structured task specification from a raw intent string.
|
|
4
|
+
* All logic is deterministic: no LLM, no external calls.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.parseIntent = parseIntent;
|
|
8
|
+
exports.buildTaskSpecificCriteria = buildTaskSpecificCriteria;
|
|
9
|
+
const ACTIONS = ["fix", "add", "update", "refactor", "remove", "migrate", "rename", "revert", "delete", "implement", "improve", "patch"];
|
|
10
|
+
const DOMAIN_KEYWORD_MAP = {
|
|
11
|
+
auth: ["auth", "login", "logout", "session", "token", "oauth", "jwt", "password", "signin", "signup", "sign-in", "sign-up", "register", "credential", "permission", "role"],
|
|
12
|
+
database: ["database", "db", "schema", "migration", "migrate", "prisma", "sql", "query", "table", "column", "index", "model", "seed", "orm"],
|
|
13
|
+
payments: ["payment", "payments", "billing", "stripe", "invoice", "subscription", "checkout", "webhook"],
|
|
14
|
+
api: ["api", "endpoint", "route", "handler", "controller", "request", "response", "rest", "graphql"],
|
|
15
|
+
ui: ["ui", "style", "css", "component", "page", "layout", "copy", "design", "render", "display", "view", "frontend", "html", "template"],
|
|
16
|
+
mcp: ["mcp", "agentbridge", "cursor", "rules"],
|
|
17
|
+
tests: ["test", "tests", "spec", "vitest", "jest", "coverage", "proof"],
|
|
18
|
+
};
|
|
19
|
+
const SYMPTOMS = {
|
|
20
|
+
"401": ["401", "unauthorized", "unauthenticated"],
|
|
21
|
+
"403": ["403", "forbidden"],
|
|
22
|
+
"404": ["404", "not found", "notfound"],
|
|
23
|
+
"500": ["500", "internal server error", "server error"],
|
|
24
|
+
"null": ["null", "undefined", "nan", "nil"],
|
|
25
|
+
"crash": ["crash", "crashes", "crashed", "exception", "throws", "throw"],
|
|
26
|
+
"loop": ["loop", "infinite loop", "recursion"],
|
|
27
|
+
"hang": ["hang", "hangs", "timeout", "deadlock"],
|
|
28
|
+
"slow": ["slow", "performance", "latency", "memory leak", "memory"],
|
|
29
|
+
"duplicate": ["duplicate", "duplicated", "twice"],
|
|
30
|
+
"missing": ["missing", "not showing", "not found"],
|
|
31
|
+
};
|
|
32
|
+
// Route pattern: /something or /something/else (no file extension)
|
|
33
|
+
const ROUTE_RE = /(?<!\w)(\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_:_-]+)*)/g;
|
|
34
|
+
// File with extension: something.ts, SomeThing.tsx, etc.
|
|
35
|
+
const FILE_RE = /\b([A-Za-z][A-Za-z0-9_-]*\.[a-z]{2,4})\b/g;
|
|
36
|
+
// PascalCase class or model name (≥2 caps or starts with uppercase followed by lowercase+uppercase)
|
|
37
|
+
const PASCAL_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]*)+)\b/g;
|
|
38
|
+
// camelCase function name — at least one internal uppercase letter, no spaces
|
|
39
|
+
const CAMEL_FN_RE = /\b([a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)+)\b/g;
|
|
40
|
+
// Quoted phrase
|
|
41
|
+
const QUOTED_RE = /"([^"]+)"|'([^']+)'/g;
|
|
42
|
+
function detectDomain(lower) {
|
|
43
|
+
for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORD_MAP)) {
|
|
44
|
+
if (keywords.some((kw) => lower.includes(kw)))
|
|
45
|
+
return domain;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function detectAction(lower) {
|
|
50
|
+
for (const action of ACTIONS) {
|
|
51
|
+
if (lower.startsWith(action) || lower.includes(` ${action} `))
|
|
52
|
+
return action;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function detectSymptom(lower) {
|
|
57
|
+
for (const [symptom, patterns] of Object.entries(SYMPTOMS)) {
|
|
58
|
+
if (patterns.some((p) => lower.includes(p)))
|
|
59
|
+
return symptom;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function extractTargets(raw) {
|
|
64
|
+
const targets = new Set();
|
|
65
|
+
// Quoted phrases first
|
|
66
|
+
for (const m of raw.matchAll(QUOTED_RE)) {
|
|
67
|
+
const phrase = m[1] ?? m[2];
|
|
68
|
+
if (phrase)
|
|
69
|
+
targets.add(phrase.trim());
|
|
70
|
+
}
|
|
71
|
+
// API routes
|
|
72
|
+
for (const m of raw.matchAll(ROUTE_RE)) {
|
|
73
|
+
targets.add(m[1]);
|
|
74
|
+
}
|
|
75
|
+
// Files with extensions
|
|
76
|
+
for (const m of raw.matchAll(FILE_RE)) {
|
|
77
|
+
targets.add(m[1]);
|
|
78
|
+
}
|
|
79
|
+
// PascalCase (models, classes)
|
|
80
|
+
for (const m of raw.matchAll(PASCAL_RE)) {
|
|
81
|
+
targets.add(m[1]);
|
|
82
|
+
}
|
|
83
|
+
// camelCase function names — only when short enough to be a name, not prose
|
|
84
|
+
for (const m of raw.matchAll(CAMEL_FN_RE)) {
|
|
85
|
+
if (m[1].length <= 40)
|
|
86
|
+
targets.add(m[1]);
|
|
87
|
+
}
|
|
88
|
+
return [...targets];
|
|
89
|
+
}
|
|
90
|
+
function inferLayers(domain, targets, symptom, action, rawLower) {
|
|
91
|
+
const layers = [];
|
|
92
|
+
const hasRoute = targets.some((t) => t.startsWith("/"));
|
|
93
|
+
const hasMigration = action === "migrate" || targets.some((t) => t.toLowerCase().includes("migration"));
|
|
94
|
+
if (domain === "auth") {
|
|
95
|
+
if (symptom === "401" || symptom === "403")
|
|
96
|
+
layers.push("route_handler", "auth_middleware");
|
|
97
|
+
else if (hasRoute)
|
|
98
|
+
layers.push("route_handler", "auth_middleware");
|
|
99
|
+
else
|
|
100
|
+
layers.push("auth_middleware");
|
|
101
|
+
}
|
|
102
|
+
if (domain === "api") {
|
|
103
|
+
if (hasRoute)
|
|
104
|
+
layers.push("route_handler");
|
|
105
|
+
else
|
|
106
|
+
layers.push("route_handler");
|
|
107
|
+
}
|
|
108
|
+
if (domain === "database") {
|
|
109
|
+
if (hasMigration)
|
|
110
|
+
layers.push("schema", "migration_file");
|
|
111
|
+
else
|
|
112
|
+
layers.push("model", "query");
|
|
113
|
+
}
|
|
114
|
+
if (domain === "ui") {
|
|
115
|
+
layers.push("component", "view");
|
|
116
|
+
}
|
|
117
|
+
if (domain === "payments") {
|
|
118
|
+
if (targets.some((t) => t.toLowerCase().includes("webhook")) || rawLower.includes("webhook")) {
|
|
119
|
+
layers.push("webhook_handler");
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
layers.push("billing_handler");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (domain === "mcp") {
|
|
126
|
+
layers.push("mcp_server", "config");
|
|
127
|
+
}
|
|
128
|
+
return layers;
|
|
129
|
+
}
|
|
130
|
+
function parseIntent(raw) {
|
|
131
|
+
if (!raw?.trim()) {
|
|
132
|
+
return { raw: raw ?? "", domain: null, targets: [], symptom: null, action: null, affected_layer: [] };
|
|
133
|
+
}
|
|
134
|
+
const lower = raw.toLowerCase();
|
|
135
|
+
const domain = detectDomain(lower);
|
|
136
|
+
const action = detectAction(lower);
|
|
137
|
+
const symptom = detectSymptom(lower);
|
|
138
|
+
const targets = extractTargets(raw);
|
|
139
|
+
const affected_layer = inferLayers(domain, targets, symptom, action, lower);
|
|
140
|
+
return { raw, domain, targets, symptom, action, affected_layer };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Builds task-specific criteria to prepend/append to a profile's generic criteria.
|
|
144
|
+
* Returns at most 2 sentences so the checklist stays readable.
|
|
145
|
+
*/
|
|
146
|
+
function buildTaskSpecificCriteria(parsed) {
|
|
147
|
+
const extra = [];
|
|
148
|
+
const routeTargets = parsed.targets.filter((t) => t.startsWith("/"));
|
|
149
|
+
const fileTargets = parsed.targets.filter((t) => t.includes("."));
|
|
150
|
+
const namedTargets = parsed.targets.filter((t) => !t.startsWith("/") && !t.includes("."));
|
|
151
|
+
// Task-specific entry criterion (what to look at)
|
|
152
|
+
if (routeTargets.length > 0 && parsed.domain === "auth" && parsed.symptom) {
|
|
153
|
+
extra.push(`The ${routeTargets[0]} route handler or its auth middleware chain is updated to address the ${parsed.symptom}.`);
|
|
154
|
+
}
|
|
155
|
+
else if (routeTargets.length > 0 && parsed.domain === "api") {
|
|
156
|
+
extra.push(`The ${routeTargets[0]} endpoint handler is updated as intended.`);
|
|
157
|
+
}
|
|
158
|
+
else if (routeTargets.length > 0) {
|
|
159
|
+
extra.push(`Changes are focused on the ${routeTargets[0]} route.`);
|
|
160
|
+
}
|
|
161
|
+
else if (fileTargets.length > 0) {
|
|
162
|
+
extra.push(`Changes are focused on ${fileTargets[0]}.`);
|
|
163
|
+
}
|
|
164
|
+
else if (namedTargets.length > 0) {
|
|
165
|
+
extra.push(`Changes are focused on ${namedTargets[0]}.`);
|
|
166
|
+
}
|
|
167
|
+
// Task-specific proof criterion (what to verify)
|
|
168
|
+
if (parsed.symptom && routeTargets.length > 0) {
|
|
169
|
+
extra.push(`Proof exists for the ${parsed.symptom} behavior on ${routeTargets[0]}.`);
|
|
170
|
+
}
|
|
171
|
+
else if (parsed.symptom) {
|
|
172
|
+
extra.push(`Proof exists that the ${parsed.symptom} condition is resolved.`);
|
|
173
|
+
}
|
|
174
|
+
else if (parsed.action && parsed.domain) {
|
|
175
|
+
extra.push(`Proof exists that the ${parsed.action} to the ${parsed.domain} layer is correct.`);
|
|
176
|
+
}
|
|
177
|
+
return extra;
|
|
178
|
+
}
|