projscan 4.0.0 → 4.2.0
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 +480 -24
- package/dist/cli/commands/route.js +1 -0
- package/dist/cli/commands/route.js.map +1 -1
- package/dist/cli/commands/semanticGraph.js +27 -0
- package/dist/cli/commands/semanticGraph.js.map +1 -1
- package/dist/cli/commands/start.js +1095 -2
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/core/dependencyAnalyzer.js +172 -0
- package/dist/core/dependencyAnalyzer.js.map +1 -1
- package/dist/core/intentRouter.d.ts +8 -1
- package/dist/core/intentRouter.js +2186 -22
- package/dist/core/intentRouter.js.map +1 -1
- package/dist/core/issueEngine.js +6 -7
- package/dist/core/issueEngine.js.map +1 -1
- package/dist/core/onboarding.d.ts +2 -2
- package/dist/core/onboarding.js +29 -5
- package/dist/core/onboarding.js.map +1 -1
- package/dist/core/start.d.ts +1 -0
- package/dist/core/start.js +3047 -10
- package/dist/core/start.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +14 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/start.js +6 -1
- package/dist/mcp/tools/start.js.map +1 -1
- package/dist/projscan-sbom.cdx.json +6 -6
- package/dist/reporters/consoleReporter.js +19 -0
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.js +19 -0
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/tool-manifest.json +6 -2
- package/dist/types.d.ts +275 -0
- package/docs/GUIDE.md +1567 -0
- package/docs/ROADMAP.md +219 -0
- package/docs/demos/projscan-4-1-demo.html +677 -0
- package/docs/projscan-mission-control.png +0 -0
- package/docs/projscan-proof-router.png +0 -0
- package/package.json +8 -1
- package/scripts/capture-readme-assets.mjs +60 -0
package/dist/core/start.js
CHANGED
|
@@ -4,11 +4,15 @@ import { fixFirstFromStartRisk } from './fixFirst.js';
|
|
|
4
4
|
import { buildFirstTenMinutes } from './onboarding.js';
|
|
5
5
|
import { computeQualityScorecard } from './qualityScorecard.js';
|
|
6
6
|
import { buildWorkplanHandoff, computeWorkplan, isWorkplanMode } from './workplan.js';
|
|
7
|
+
import { routeIntent } from './intentRouter.js';
|
|
7
8
|
import { getChangedFiles } from '../utils/changedFiles.js';
|
|
8
9
|
const DEFAULT_MAX_TASKS = 5;
|
|
9
10
|
const DEFAULT_MAX_RISKS = 5;
|
|
11
|
+
const READY_PROOF_SUMMARY = 'Ready-to-run proof commands; placeholder follow-ups are excluded until Needs Input is resolved.';
|
|
10
12
|
export async function computeStartReport(rootPath, options = {}) {
|
|
11
|
-
const
|
|
13
|
+
const intent = normalizeIntent(options.intent);
|
|
14
|
+
const modeResolution = resolveStartMode(options.mode, intent);
|
|
15
|
+
const mode = modeResolution.mode;
|
|
12
16
|
const maxTasks = normalizeLimit(options.maxTasks, DEFAULT_MAX_TASKS, 12);
|
|
13
17
|
const maxRisks = normalizeLimit(options.maxRisks, DEFAULT_MAX_RISKS, 12);
|
|
14
18
|
const [setup, workplan, quality, riskSources] = await Promise.all([
|
|
@@ -30,9 +34,21 @@ export async function computeStartReport(rootPath, options = {}) {
|
|
|
30
34
|
...(diagnostic.command ? { command: diagnostic.command } : {}),
|
|
31
35
|
}));
|
|
32
36
|
const adoptionLoop = buildAdoptionLoop();
|
|
33
|
-
const firstTenMinutes = buildFirstTenMinutes();
|
|
34
|
-
const coordinationHints = buildStartCoordinationHints(riskSources);
|
|
37
|
+
const firstTenMinutes = buildFirstTenMinutes(mode);
|
|
38
|
+
const coordinationHints = buildStartCoordinationHints(riskSources, mode);
|
|
39
|
+
const missionControl = buildMissionControl({
|
|
40
|
+
mode,
|
|
41
|
+
intent,
|
|
42
|
+
setupOverall: setup.overall,
|
|
43
|
+
workplan,
|
|
44
|
+
workflow,
|
|
45
|
+
fixFirst,
|
|
46
|
+
adoptionGaps,
|
|
47
|
+
coordinationHints,
|
|
48
|
+
riskSources,
|
|
49
|
+
});
|
|
35
50
|
const nextActions = dedupeActions([
|
|
51
|
+
missionControl.primaryAction,
|
|
36
52
|
...firstTenMinutes.commands.map((step) => ({ label: `First 10 minutes: ${step.label}`, command: step.command })),
|
|
37
53
|
...workflow.commands.map((command) => ({ label: `Run ${workflow.name}`, command })),
|
|
38
54
|
...adoptionLoop.nextCommands.map((command) => ({ label: 'Keep using projscan every PR', command })),
|
|
@@ -44,6 +60,8 @@ export async function computeStartReport(rootPath, options = {}) {
|
|
|
44
60
|
readOnly: true,
|
|
45
61
|
rootPath,
|
|
46
62
|
mode,
|
|
63
|
+
modeSource: modeResolution.source,
|
|
64
|
+
modeReason: modeResolution.reason,
|
|
47
65
|
summary: summarize(mode, workplan, quality.topRisks.length, adoptionGaps.length, fixFirst?.title),
|
|
48
66
|
setup: {
|
|
49
67
|
overall: setup.overall,
|
|
@@ -51,6 +69,7 @@ export async function computeStartReport(rootPath, options = {}) {
|
|
|
51
69
|
},
|
|
52
70
|
recommendedWorkflow: workflow,
|
|
53
71
|
firstTenMinutes,
|
|
72
|
+
missionControl,
|
|
54
73
|
coordinationHints,
|
|
55
74
|
evidence: {
|
|
56
75
|
workplanVerdict: workplan.verdict,
|
|
@@ -71,13 +90,14 @@ export async function computeStartReport(rootPath, options = {}) {
|
|
|
71
90
|
};
|
|
72
91
|
return report;
|
|
73
92
|
}
|
|
74
|
-
function buildStartCoordinationHints(riskSources) {
|
|
93
|
+
function buildStartCoordinationHints(riskSources, mode) {
|
|
94
|
+
const preflightMode = preflightModeForMission(mode);
|
|
75
95
|
const hints = [
|
|
76
96
|
{
|
|
77
97
|
id: 'current-worktree-check',
|
|
78
98
|
label: 'Separate current worktree evidence from session memory',
|
|
79
99
|
message: `Current worktree evidence sees ${riskSources.currentWorktree.count} changed file(s); remembered session context may include older agent touches.`,
|
|
80
|
-
command:
|
|
100
|
+
command: `projscan preflight --mode ${preflightMode} --format json`,
|
|
81
101
|
},
|
|
82
102
|
];
|
|
83
103
|
if (riskSources.sessionMemory.totalTouchedFiles > 0) {
|
|
@@ -167,16 +187,3030 @@ function buildAdoptionLoop() {
|
|
|
167
187
|
],
|
|
168
188
|
};
|
|
169
189
|
}
|
|
170
|
-
function
|
|
171
|
-
if (typeof value === 'string' && isWorkplanMode(value))
|
|
172
|
-
return
|
|
173
|
-
|
|
190
|
+
function resolveStartMode(value, intent) {
|
|
191
|
+
if (typeof value === 'string' && isWorkplanMode(value)) {
|
|
192
|
+
return {
|
|
193
|
+
mode: value,
|
|
194
|
+
source: 'explicit',
|
|
195
|
+
reason: `Mode ${value} was provided explicitly.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const inferred = inferModeFromIntent(intent);
|
|
199
|
+
if (inferred) {
|
|
200
|
+
return {
|
|
201
|
+
mode: inferred,
|
|
202
|
+
source: 'intent',
|
|
203
|
+
reason: `Intent "${intent}" maps to the ${inferred} workflow.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const routed = routesForIntent(intent).length > 0;
|
|
207
|
+
return {
|
|
208
|
+
mode: 'before_edit',
|
|
209
|
+
source: 'default',
|
|
210
|
+
reason: intent
|
|
211
|
+
? routed
|
|
212
|
+
? `Mission Control routed the intent, but no workflow-mode hint matched "${intent}", so start defaults to before_edit.`
|
|
213
|
+
: `No mode-specific intent matched "${intent}", so start defaults to before_edit.`
|
|
214
|
+
: 'No mode-specific intent or explicit mode was supplied, so start defaults to before_edit.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function inferModeFromIntent(intent) {
|
|
218
|
+
const routes = routesForIntent(intent);
|
|
219
|
+
const primaryRoute = routes[0];
|
|
220
|
+
if (primaryRoute?.tool === 'projscan_release_train')
|
|
221
|
+
return 'release';
|
|
222
|
+
if (primaryRoute?.tool === 'projscan_bug_hunt' && primaryRoute.confidence === 'high')
|
|
223
|
+
return 'bug_hunt';
|
|
224
|
+
if (primaryRoute?.tool === 'projscan_dataflow' && primaryRoute.confidence === 'high')
|
|
225
|
+
return 'hardening';
|
|
226
|
+
if (primaryRoute?.tool === 'projscan_evidence_pack')
|
|
227
|
+
return 'before_commit';
|
|
228
|
+
if (primaryRoute?.tool === 'projscan_review')
|
|
229
|
+
return reviewModeFromIntent(intent ?? '');
|
|
230
|
+
if (primaryRoute?.tool === 'projscan_regression_plan')
|
|
231
|
+
return regressionModeFromIntent(intent ?? '');
|
|
232
|
+
if (primaryRoute?.tool === 'projscan_pr_diff')
|
|
233
|
+
return 'before_commit';
|
|
234
|
+
if (primaryRoute?.tool === 'projscan_merge_risk')
|
|
235
|
+
return 'before_merge';
|
|
236
|
+
if (primaryRoute?.tool === 'projscan_preflight')
|
|
237
|
+
return preflightModeFromIntent(intent ?? '');
|
|
238
|
+
if (routes.some((route) => route.tool === 'projscan_preflight') && hasPreflightModeHint(intent ?? '')) {
|
|
239
|
+
return preflightModeFromIntent(intent ?? '');
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
function hasPreflightModeHint(intent) {
|
|
244
|
+
return /\b(?:safe|safety|gate|preflight|commit|committing|committed|merge|merged|merging|rebase|rebasing|conflict|conflicts|resolve|resolving|edit|proceed|block|blocked|blocker|blockers|blocking|allowed)\b/i.test(intent);
|
|
245
|
+
}
|
|
246
|
+
function normalizeIntent(value) {
|
|
247
|
+
if (typeof value !== 'string')
|
|
248
|
+
return undefined;
|
|
249
|
+
const trimmed = value.trim().replace(/\s+/g, ' ');
|
|
250
|
+
if (trimmed.length === 0)
|
|
251
|
+
return undefined;
|
|
252
|
+
return trimmed.slice(0, 240);
|
|
174
253
|
}
|
|
175
254
|
function normalizeLimit(value, fallback, max) {
|
|
176
255
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
177
256
|
return fallback;
|
|
178
257
|
return Math.max(1, Math.min(max, Math.floor(value)));
|
|
179
258
|
}
|
|
259
|
+
function buildMissionControl(input) {
|
|
260
|
+
const routeCandidates = routesForIntent(input.intent);
|
|
261
|
+
const routed = routeCandidates[0];
|
|
262
|
+
const alternatives = routeCandidates.slice(1, 4);
|
|
263
|
+
const status = missionStatus(input.setupOverall, input.workplan.verdict, input.adoptionGaps);
|
|
264
|
+
const actionPlan = missionActionPlan(input.intent, routed, input.fixFirst, input.workplan, input.workflow);
|
|
265
|
+
const primaryAction = actionPlan[0] ?? actionFromWorkflow(input.workflow);
|
|
266
|
+
const readyActions = missionReadyActions(actionPlan);
|
|
267
|
+
const guardrails = missionGuardrails(input.mode, input.coordinationHints, primaryAction);
|
|
268
|
+
const proofCommands = missionProofCommands(input.mode, input.workplan, guardrails, actionPlan);
|
|
269
|
+
const successCriteria = missionSuccessCriteria(input.mode, routed, actionPlan, input.workplan);
|
|
270
|
+
const unresolvedInputs = missionUnresolvedInputs(actionPlan);
|
|
271
|
+
const executionPlan = buildMissionExecutionPlan({
|
|
272
|
+
primaryAction,
|
|
273
|
+
actionPlan,
|
|
274
|
+
readyActions,
|
|
275
|
+
unresolvedInputs,
|
|
276
|
+
successCriteria,
|
|
277
|
+
proofCommands,
|
|
278
|
+
});
|
|
279
|
+
const resume = missionResume(executionPlan);
|
|
280
|
+
const reviewProof = buildMissionReviewProof(resume, proofCommands);
|
|
281
|
+
const whyNow = routed
|
|
282
|
+
? routedWhyNow(routed, actionPlan)
|
|
283
|
+
: input.fixFirst
|
|
284
|
+
? `Top evidence points to "${input.fixFirst.title}" as the first useful move.`
|
|
285
|
+
: `The ${input.mode} workflow is the shortest path from orientation to verified action.`;
|
|
286
|
+
const reviewGate = buildMissionReviewGate({
|
|
287
|
+
status,
|
|
288
|
+
doneWhen: successCriteria,
|
|
289
|
+
proof: reviewProof,
|
|
290
|
+
currentWorktree: input.riskSources.currentWorktree,
|
|
291
|
+
});
|
|
292
|
+
const handoffPrompt = missionHandoffPrompt(resume, successCriteria, whyNow, unresolvedInputs, proofCommands, reviewGate);
|
|
293
|
+
const runbook = buildMissionRunbook({
|
|
294
|
+
intent: input.intent,
|
|
295
|
+
status,
|
|
296
|
+
primaryAction,
|
|
297
|
+
readyActions,
|
|
298
|
+
unresolvedInputs,
|
|
299
|
+
successCriteria,
|
|
300
|
+
proofCommands,
|
|
301
|
+
executionPlan,
|
|
302
|
+
resume,
|
|
303
|
+
handoffPrompt,
|
|
304
|
+
reviewGate,
|
|
305
|
+
});
|
|
306
|
+
const taskCard = buildMissionTaskCard({
|
|
307
|
+
intent: input.intent,
|
|
308
|
+
status,
|
|
309
|
+
currentStep: executionPlan.cursor,
|
|
310
|
+
resume,
|
|
311
|
+
successCriteria,
|
|
312
|
+
handoffPrompt,
|
|
313
|
+
reviewGate,
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
...(input.intent ? { intent: input.intent } : {}),
|
|
317
|
+
status,
|
|
318
|
+
headline: headlineForStatus(status, primaryAction.label),
|
|
319
|
+
whyNow,
|
|
320
|
+
primaryAction,
|
|
321
|
+
actionPlan,
|
|
322
|
+
readyActions,
|
|
323
|
+
...(routed ? { routedIntent: routed } : {}),
|
|
324
|
+
...(alternatives.length > 0 ? { alternatives } : {}),
|
|
325
|
+
unresolvedInputs,
|
|
326
|
+
guardrails,
|
|
327
|
+
successCriteria,
|
|
328
|
+
proofSummary: READY_PROOF_SUMMARY,
|
|
329
|
+
proofCommands,
|
|
330
|
+
resume,
|
|
331
|
+
handoff: missionHandoff(executionPlan.cursor, resume, primaryAction, readyActions, unresolvedInputs, successCriteria, proofCommands, reviewGate),
|
|
332
|
+
executionPlan,
|
|
333
|
+
runbook,
|
|
334
|
+
reviewGate,
|
|
335
|
+
taskCard,
|
|
336
|
+
handoffPrompt,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function buildMissionReviewGate(input) {
|
|
340
|
+
const checklist = [
|
|
341
|
+
'Complete this task card and remaining proof.',
|
|
342
|
+
'Capture `git status --short`.',
|
|
343
|
+
'Capture `git diff --stat`.',
|
|
344
|
+
'Stop and ask for approval before starting another slice, release, publish, or deploy.',
|
|
345
|
+
];
|
|
346
|
+
const commands = ['git status --short', 'git diff --stat'];
|
|
347
|
+
const doneWhen = input.doneWhen.slice();
|
|
348
|
+
const policy = buildMissionReviewPolicy();
|
|
349
|
+
const decisions = buildMissionReviewDecisions();
|
|
350
|
+
const worktree = buildMissionReviewWorktree(input.currentWorktree);
|
|
351
|
+
const stopCondition = 'Stop after the current Mission Control checklist and proof are complete.';
|
|
352
|
+
const reviewPrompt = `Review the completed mission, proof output, and working-tree summary before approving another slice, release, publish, or deploy. ${input.proof.summary}`;
|
|
353
|
+
return {
|
|
354
|
+
title: 'Mission Review Gate',
|
|
355
|
+
required: true,
|
|
356
|
+
status: input.status,
|
|
357
|
+
stopCondition,
|
|
358
|
+
reviewPrompt,
|
|
359
|
+
checklist,
|
|
360
|
+
doneWhen,
|
|
361
|
+
policy,
|
|
362
|
+
decisions,
|
|
363
|
+
commands,
|
|
364
|
+
worktree,
|
|
365
|
+
proof: input.proof,
|
|
366
|
+
markdown: renderMissionReviewGateMarkdown({
|
|
367
|
+
status: input.status,
|
|
368
|
+
stopCondition,
|
|
369
|
+
reviewPrompt,
|
|
370
|
+
checklist,
|
|
371
|
+
doneWhen,
|
|
372
|
+
policy,
|
|
373
|
+
decisions,
|
|
374
|
+
commands,
|
|
375
|
+
worktree,
|
|
376
|
+
proof: input.proof,
|
|
377
|
+
}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function buildMissionReviewPolicy() {
|
|
381
|
+
return {
|
|
382
|
+
approvalRequired: true,
|
|
383
|
+
blockedActions: ['next_slice', 'release', 'publish', 'deploy', 'push', 'merge', 'version_bump'],
|
|
384
|
+
summary: 'Explicit reviewer approval is required before another slice, release, publish, deploy, push, merge, or version bump.',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function buildMissionReviewDecisions() {
|
|
388
|
+
return [
|
|
389
|
+
{
|
|
390
|
+
id: 'approve_next_slice',
|
|
391
|
+
label: 'Approve next slice',
|
|
392
|
+
description: 'The agent may start another bounded implementation slice.',
|
|
393
|
+
consequence: 'No release, publish, deploy, or version bump is allowed unless the reviewer asks for it.',
|
|
394
|
+
reply: 'Approved: start one more bounded implementation slice. Do not release, publish, deploy, push, merge, or bump the version.',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: 'request_changes',
|
|
398
|
+
label: 'Request changes',
|
|
399
|
+
description: 'The agent must address review feedback before starting more scope.',
|
|
400
|
+
consequence: 'The current mission stays open until feedback and proof are updated.',
|
|
401
|
+
reply: 'Changes requested: address the review feedback first, update proof, then stop for another review.',
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'review_version_candidate',
|
|
405
|
+
label: 'Review version candidate',
|
|
406
|
+
description: 'The agent may prepare release notes, version rationale, and remaining gates for review.',
|
|
407
|
+
consequence: 'Publishing still requires a separate explicit approval.',
|
|
408
|
+
reply: 'Prepare a version-candidate review only. Do not publish, deploy, push, merge, or bump the version.',
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
function buildMissionReviewProof(resume, proofCommands) {
|
|
413
|
+
const commands = resume.remainingProofCommands ?? proofCommands;
|
|
414
|
+
const toolCalls = resume.remainingProofToolCalls ?? [];
|
|
415
|
+
const items = resume.remainingProofItems ?? [];
|
|
416
|
+
return {
|
|
417
|
+
summary: READY_PROOF_SUMMARY,
|
|
418
|
+
commands,
|
|
419
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
420
|
+
...(items.length > 0 ? { items } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function buildMissionReviewWorktree(currentWorktree) {
|
|
424
|
+
if (!currentWorktree.available) {
|
|
425
|
+
const reason = currentWorktree.reason ?? 'unknown';
|
|
426
|
+
return {
|
|
427
|
+
available: false,
|
|
428
|
+
clean: false,
|
|
429
|
+
changedFileCount: 0,
|
|
430
|
+
files: [],
|
|
431
|
+
baseRef: currentWorktree.baseRef,
|
|
432
|
+
summary: `Current worktree evidence is unavailable: ${reason}.`,
|
|
433
|
+
reason,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const changedFileCount = currentWorktree.count;
|
|
437
|
+
const baseRef = currentWorktree.baseRef;
|
|
438
|
+
return {
|
|
439
|
+
available: true,
|
|
440
|
+
clean: changedFileCount === 0,
|
|
441
|
+
changedFileCount,
|
|
442
|
+
files: currentWorktree.files,
|
|
443
|
+
baseRef,
|
|
444
|
+
summary: changedFileCount === 0
|
|
445
|
+
? 'Current worktree evidence sees no changed files.'
|
|
446
|
+
: `Current worktree evidence sees ${changedFileCount} changed file(s)${baseRef ? ` against ${baseRef}` : ''}.`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function renderMissionReviewGateMarkdown(input) {
|
|
450
|
+
const lines = [
|
|
451
|
+
'# Mission Review Gate',
|
|
452
|
+
'',
|
|
453
|
+
`Status: ${input.status}`,
|
|
454
|
+
`Stop condition: ${input.stopCondition}`,
|
|
455
|
+
'',
|
|
456
|
+
'## Checklist',
|
|
457
|
+
...input.checklist.map((item) => `- [ ] ${item}`),
|
|
458
|
+
'',
|
|
459
|
+
'## Review Policy',
|
|
460
|
+
`Approval required: ${input.policy.approvalRequired ? 'yes' : 'no'}`,
|
|
461
|
+
input.policy.summary,
|
|
462
|
+
'Blocked until approval:',
|
|
463
|
+
...input.policy.blockedActions.map(formatMissionReviewBlockedAction),
|
|
464
|
+
'',
|
|
465
|
+
'## Done When',
|
|
466
|
+
...(input.doneWhen.length > 0
|
|
467
|
+
? input.doneWhen.map((criterion) => `- [ ] ${criterion}`)
|
|
468
|
+
: ['- [ ] The current mission is complete and verified.']),
|
|
469
|
+
'',
|
|
470
|
+
'## Reviewer Decision',
|
|
471
|
+
...input.decisions.map(formatMissionReviewDecision),
|
|
472
|
+
'',
|
|
473
|
+
...renderMissionReviewProofLines(input.proof),
|
|
474
|
+
'## Evidence Commands',
|
|
475
|
+
...input.commands.map((command) => `- \`${command}\``),
|
|
476
|
+
'',
|
|
477
|
+
'## Worktree Evidence',
|
|
478
|
+
input.worktree.summary,
|
|
479
|
+
...input.worktree.files.slice(0, 8).map((file) => `- \`${file}\``),
|
|
480
|
+
'',
|
|
481
|
+
'## Review Prompt',
|
|
482
|
+
input.reviewPrompt,
|
|
483
|
+
];
|
|
484
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
485
|
+
}
|
|
486
|
+
function formatMissionReviewDecision(decision) {
|
|
487
|
+
return `- [ ] ${decision.label}: ${decision.description} Consequence: ${decision.consequence} Reply: "${decision.reply}"`;
|
|
488
|
+
}
|
|
489
|
+
function formatMissionReviewBlockedAction(action) {
|
|
490
|
+
const labels = {
|
|
491
|
+
next_slice: 'Start another implementation slice',
|
|
492
|
+
release: 'Release',
|
|
493
|
+
publish: 'Publish',
|
|
494
|
+
deploy: 'Deploy',
|
|
495
|
+
push: 'Push',
|
|
496
|
+
merge: 'Merge',
|
|
497
|
+
version_bump: 'Version bump',
|
|
498
|
+
};
|
|
499
|
+
return `- ${labels[action]} (\`${action}\`)`;
|
|
500
|
+
}
|
|
501
|
+
function renderMissionReviewProofLines(proof) {
|
|
502
|
+
const lines = ['## Proof Queue', proof.summary];
|
|
503
|
+
if (proof.items && proof.items.length > 0) {
|
|
504
|
+
return [...lines, ...proof.items.map(formatMissionReviewProofItem), ''];
|
|
505
|
+
}
|
|
506
|
+
if (proof.commands.length > 0) {
|
|
507
|
+
return [...lines, ...proof.commands.map((command) => `- \`${command}\``), ''];
|
|
508
|
+
}
|
|
509
|
+
return [...lines, 'No proof commands are ready yet.', ''];
|
|
510
|
+
}
|
|
511
|
+
function formatMissionReviewProofItem(item) {
|
|
512
|
+
const annotation = item.toolCall
|
|
513
|
+
? ` (MCP: ${formatMissionReviewToolCall(item.toolCall)})`
|
|
514
|
+
: ' (CLI only)';
|
|
515
|
+
return `- \`${item.command}\`${annotation}`;
|
|
516
|
+
}
|
|
517
|
+
function formatMissionReviewToolCall(toolCall) {
|
|
518
|
+
return typeof toolCall.args !== 'undefined'
|
|
519
|
+
? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
|
|
520
|
+
: toolCall.tool;
|
|
521
|
+
}
|
|
522
|
+
function buildMissionRunbook(input) {
|
|
523
|
+
const readyCommands = uniqueStrings(input.readyActions
|
|
524
|
+
.map((action) => action.command ?? '')
|
|
525
|
+
.filter(isRunnableCommand));
|
|
526
|
+
const readyCommandBlock = readyCommands.join('\n');
|
|
527
|
+
const blockedInputSummary = input.unresolvedInputs.length > 0
|
|
528
|
+
? `Needs input: ${input.unresolvedInputs.map((item) => `${item.name}=${item.placeholder}`).join(', ')}.`
|
|
529
|
+
: undefined;
|
|
530
|
+
return {
|
|
531
|
+
title: `Runbook: ${input.primaryAction.label}`,
|
|
532
|
+
status: input.status,
|
|
533
|
+
currentPhase: input.executionPlan.currentPhase,
|
|
534
|
+
currentStep: input.executionPlan.cursor,
|
|
535
|
+
resume: input.resume,
|
|
536
|
+
readyCommandBlock,
|
|
537
|
+
...(blockedInputSummary ? { blockedInputSummary } : {}),
|
|
538
|
+
markdown: renderMissionRunbookMarkdown({
|
|
539
|
+
intent: input.intent,
|
|
540
|
+
status: input.status,
|
|
541
|
+
currentPhase: input.executionPlan.currentPhase,
|
|
542
|
+
currentStep: input.executionPlan.cursor,
|
|
543
|
+
resume: input.resume,
|
|
544
|
+
primaryAction: input.primaryAction,
|
|
545
|
+
readyCommands,
|
|
546
|
+
unresolvedInputs: input.unresolvedInputs,
|
|
547
|
+
proofCommands: input.proofCommands,
|
|
548
|
+
successCriteria: input.successCriteria,
|
|
549
|
+
handoffPrompt: input.handoffPrompt,
|
|
550
|
+
reviewGate: input.reviewGate,
|
|
551
|
+
}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function renderMissionRunbookMarkdown(input) {
|
|
555
|
+
const lines = [
|
|
556
|
+
'# Mission Runbook',
|
|
557
|
+
'',
|
|
558
|
+
...(input.intent ? [`Intent: ${input.intent}`] : []),
|
|
559
|
+
`Status: ${input.status}`,
|
|
560
|
+
`Current phase: ${input.currentPhase}`,
|
|
561
|
+
`Next action: ${input.primaryAction.command ? `\`${input.primaryAction.command}\`` : input.primaryAction.label}`,
|
|
562
|
+
'',
|
|
563
|
+
...renderRunbookCursorLines(input.currentStep),
|
|
564
|
+
'',
|
|
565
|
+
...renderRunbookResumeLines(input.resume),
|
|
566
|
+
'',
|
|
567
|
+
'## Handoff Prompt',
|
|
568
|
+
input.handoffPrompt,
|
|
569
|
+
'',
|
|
570
|
+
'## Review Gate',
|
|
571
|
+
...input.reviewGate.checklist.map((item) => `- [ ] ${item}`),
|
|
572
|
+
'',
|
|
573
|
+
'## Reviewer Decision',
|
|
574
|
+
...input.reviewGate.decisions.map(formatMissionReviewDecision),
|
|
575
|
+
'',
|
|
576
|
+
input.reviewGate.reviewPrompt,
|
|
577
|
+
'',
|
|
578
|
+
'## Ready Commands',
|
|
579
|
+
...(input.readyCommands.length > 0 ? input.readyCommands.map((command) => `- \`${command}\``) : ['- None yet. Resolve blocked inputs first.']),
|
|
580
|
+
'',
|
|
581
|
+
...(input.unresolvedInputs.length > 0
|
|
582
|
+
? [
|
|
583
|
+
'## Blocked Inputs',
|
|
584
|
+
...input.unresolvedInputs.map((item) => `- ${item.name}: ${item.instruction}`),
|
|
585
|
+
'',
|
|
586
|
+
]
|
|
587
|
+
: []),
|
|
588
|
+
'## Proof Commands',
|
|
589
|
+
...(input.proofCommands.length > 0 ? input.proofCommands.map((command) => `- \`${command}\``) : ['- No proof commands available yet.']),
|
|
590
|
+
'',
|
|
591
|
+
'## Done When',
|
|
592
|
+
...(input.successCriteria.length > 0 ? input.successCriteria.map((criterion) => `- ${criterion}`) : ['- The next action is complete and verified.']),
|
|
593
|
+
];
|
|
594
|
+
return `${lines.join('\n')}\n`;
|
|
595
|
+
}
|
|
596
|
+
function buildMissionTaskCard(input) {
|
|
597
|
+
return {
|
|
598
|
+
title: 'Mission Task Card',
|
|
599
|
+
status: input.status,
|
|
600
|
+
currentPhase: input.currentStep.phaseId,
|
|
601
|
+
currentStep: input.currentStep,
|
|
602
|
+
markdown: renderMissionTaskCardMarkdown(input),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function renderMissionTaskCardMarkdown(input) {
|
|
606
|
+
const lines = [
|
|
607
|
+
'# Mission Task Card',
|
|
608
|
+
'',
|
|
609
|
+
...(input.intent ? [`Intent: ${input.intent}`] : []),
|
|
610
|
+
`Status: ${input.status}`,
|
|
611
|
+
`Current step: ${input.currentStep.stepId} in ${input.currentStep.phaseId}`,
|
|
612
|
+
'',
|
|
613
|
+
'## Do Next',
|
|
614
|
+
...missionTaskCardActionLines(input.resume),
|
|
615
|
+
'',
|
|
616
|
+
'## Proof',
|
|
617
|
+
...missionTaskCardProofLines(input.resume),
|
|
618
|
+
'',
|
|
619
|
+
'## Done When',
|
|
620
|
+
...(input.successCriteria.length > 0
|
|
621
|
+
? input.successCriteria.map((criterion) => `- [ ] ${criterion}`)
|
|
622
|
+
: ['- [ ] The next action is complete and verified.']),
|
|
623
|
+
'',
|
|
624
|
+
'## Review Gate',
|
|
625
|
+
...input.reviewGate.checklist.map((item) => `- [ ] ${item}`),
|
|
626
|
+
'',
|
|
627
|
+
'## Reviewer Decision',
|
|
628
|
+
...input.reviewGate.decisions.map(formatMissionReviewDecision),
|
|
629
|
+
'',
|
|
630
|
+
'## Handoff Prompt',
|
|
631
|
+
input.handoffPrompt,
|
|
632
|
+
];
|
|
633
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
634
|
+
}
|
|
635
|
+
function missionTaskCardActionLines(resume) {
|
|
636
|
+
const checklist = resume.checklist ?? [];
|
|
637
|
+
const actionLines = checklist
|
|
638
|
+
.filter((item) => item.kind !== 'run_proof' && item.kind !== 'confirm_done')
|
|
639
|
+
.map(formatTaskCardChecklistItem);
|
|
640
|
+
return actionLines.length > 0 ? actionLines : ['- [ ] Continue from the current Mission Control cursor.'];
|
|
641
|
+
}
|
|
642
|
+
function missionTaskCardProofLines(resume) {
|
|
643
|
+
const proofItems = resume.remainingProofItems ?? [];
|
|
644
|
+
const proofLines = proofItems.map(formatTaskCardProofItem);
|
|
645
|
+
if (proofLines.length > 0)
|
|
646
|
+
return proofLines;
|
|
647
|
+
const commands = resume.remainingProofCommands ?? [];
|
|
648
|
+
return commands.length > 0
|
|
649
|
+
? commands.map((command) => `- [ ] \`${command}\``)
|
|
650
|
+
: ['- [ ] No proof commands are ready yet.'];
|
|
651
|
+
}
|
|
652
|
+
function formatTaskCardChecklistItem(item) {
|
|
653
|
+
if (item.kind === 'resolve_input') {
|
|
654
|
+
const label = item.label ? ` (\`${item.label}\`)` : '';
|
|
655
|
+
const instruction = item.instruction ?? item.label;
|
|
656
|
+
return `- [ ] Resolve \`${item.stepId}\`${label}: ${instruction}`;
|
|
657
|
+
}
|
|
658
|
+
if (item.kind === 'run_follow_up' && item.command) {
|
|
659
|
+
const prefix = item.status === 'blocked' ? 'After inputs, run' : 'Then run';
|
|
660
|
+
return `- [ ] ${prefix} \`${item.command}\`${formatTaskCardChecklistAnnotation(item)}`;
|
|
661
|
+
}
|
|
662
|
+
if (item.command) {
|
|
663
|
+
return `- [ ] Run \`${item.command}\`${formatTaskCardChecklistAnnotation(item)}`;
|
|
664
|
+
}
|
|
665
|
+
return `- [ ] ${item.instruction ?? item.label}`;
|
|
666
|
+
}
|
|
667
|
+
function formatTaskCardChecklistAnnotation(item) {
|
|
668
|
+
if (!item.tool)
|
|
669
|
+
return '';
|
|
670
|
+
return ` (MCP: ${formatTaskCardToolCall({ tool: item.tool, ...(typeof item.args !== 'undefined' ? { args: item.args } : {}) })})`;
|
|
671
|
+
}
|
|
672
|
+
function formatTaskCardProofItem(item) {
|
|
673
|
+
const annotation = item.toolCall
|
|
674
|
+
? ` (MCP: ${formatTaskCardToolCall(item.toolCall)})`
|
|
675
|
+
: ' (CLI only)';
|
|
676
|
+
return `- [ ] \`${item.command}\`${annotation}`;
|
|
677
|
+
}
|
|
678
|
+
function formatTaskCardToolCall(toolCall) {
|
|
679
|
+
return typeof toolCall.args !== 'undefined'
|
|
680
|
+
? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
|
|
681
|
+
: toolCall.tool;
|
|
682
|
+
}
|
|
683
|
+
function missionResume(plan) {
|
|
684
|
+
const cursor = plan.cursor;
|
|
685
|
+
const commandBlock = cursor.command && isRunnableCommand(cursor.command) ? cursor.command : undefined;
|
|
686
|
+
const toolCall = resumeToolCall(plan, cursor);
|
|
687
|
+
const followUps = resumeFollowUps(plan, cursor);
|
|
688
|
+
const inputBindings = resumeInputBindings(plan, cursor);
|
|
689
|
+
const checklist = resumeChecklist(plan, cursor, inputBindings, followUps);
|
|
690
|
+
const remainingProofItems = resumeRemainingProofItems(checklist);
|
|
691
|
+
const remainingProofCommands = resumeRemainingProofCommands(checklist);
|
|
692
|
+
const remainingProofToolCalls = resumeRemainingProofToolCalls(checklist);
|
|
693
|
+
const unlocks = resolveResumeReferences(plan, cursor.unlocks);
|
|
694
|
+
const blockedBy = resolveResumeReferences(plan, cursor.blockedBy);
|
|
695
|
+
const instruction = commandBlock
|
|
696
|
+
? `Run ${commandBlock}.`
|
|
697
|
+
: cursor.instruction
|
|
698
|
+
? `Resolve ${cursor.label}: ${cursor.instruction}`
|
|
699
|
+
: `Continue with ${cursor.label}.`;
|
|
700
|
+
const prompt = commandBlock
|
|
701
|
+
? `Resume at ${cursor.stepId} in ${cursor.phaseId}: run \`${commandBlock}\`.${resumeUnlocksSentence(unlocks, cursor.unlocks)}`
|
|
702
|
+
: `Resume at ${cursor.stepId} in ${cursor.phaseId}: ${instruction}${resumeBlockersSentence(blockedBy, cursor.blockedBy)}`;
|
|
703
|
+
return {
|
|
704
|
+
currentStep: cursor,
|
|
705
|
+
status: cursor.status,
|
|
706
|
+
instruction,
|
|
707
|
+
prompt,
|
|
708
|
+
...(commandBlock ? { commandBlock } : {}),
|
|
709
|
+
...(toolCall ? { toolCall } : {}),
|
|
710
|
+
...(followUps.length > 0 ? { followUps } : {}),
|
|
711
|
+
...(inputBindings.length > 0 ? { inputBindings } : {}),
|
|
712
|
+
...(checklist.length > 0 ? { checklist } : {}),
|
|
713
|
+
...(remainingProofItems.length > 0 ? { remainingProofItems } : {}),
|
|
714
|
+
...(remainingProofCommands.length > 0 ? { remainingProofCommands } : {}),
|
|
715
|
+
...(remainingProofToolCalls.length > 0 ? { remainingProofToolCalls } : {}),
|
|
716
|
+
...(unlocks.length > 0 ? { unlocks } : {}),
|
|
717
|
+
...(blockedBy.length > 0 ? { blockedBy } : {}),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function resumeRemainingProofCommands(checklist) {
|
|
721
|
+
return checklist
|
|
722
|
+
.filter((item) => item.kind === 'run_proof' && typeof item.command === 'string')
|
|
723
|
+
.map((item) => item.command);
|
|
724
|
+
}
|
|
725
|
+
function resumeRemainingProofItems(checklist) {
|
|
726
|
+
return checklist.flatMap((item) => {
|
|
727
|
+
if (item.kind !== 'run_proof' || typeof item.command !== 'string')
|
|
728
|
+
return [];
|
|
729
|
+
const toolCall = proofChecklistToolCall(item);
|
|
730
|
+
return [{
|
|
731
|
+
stepId: item.stepId,
|
|
732
|
+
status: item.status,
|
|
733
|
+
label: item.label,
|
|
734
|
+
command: item.command,
|
|
735
|
+
...(toolCall ? { toolCall } : {}),
|
|
736
|
+
}];
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function resumeRemainingProofToolCalls(checklist) {
|
|
740
|
+
return checklist.flatMap((item) => {
|
|
741
|
+
if (item.kind !== 'run_proof' || typeof item.command !== 'string')
|
|
742
|
+
return [];
|
|
743
|
+
const toolCall = proofChecklistToolCall(item);
|
|
744
|
+
return toolCall ? [{ stepId: item.stepId, command: item.command, ...toolCall }] : [];
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function proofChecklistToolCall(item) {
|
|
748
|
+
if (item.tool) {
|
|
749
|
+
return {
|
|
750
|
+
tool: item.tool,
|
|
751
|
+
...(typeof item.args !== 'undefined' ? { args: item.args } : {}),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return typeof item.command === 'string' ? proofCommandToolCall(item.command) : undefined;
|
|
755
|
+
}
|
|
756
|
+
function proofCommandToolCall(command) {
|
|
757
|
+
const preflightMatch = /^projscan preflight(?: --mode ([a-z_]+))? --format json$/.exec(command);
|
|
758
|
+
if (preflightMatch) {
|
|
759
|
+
return {
|
|
760
|
+
tool: 'projscan_preflight',
|
|
761
|
+
args: preflightMatch[1] ? { mode: preflightMatch[1] } : {},
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const understandMatch = /^projscan understand --view ([a-z_]+)(?: --intent "((?:\\.|[^"\\])*)")? --format json$/.exec(command);
|
|
765
|
+
if (understandMatch) {
|
|
766
|
+
return {
|
|
767
|
+
tool: 'projscan_understand',
|
|
768
|
+
args: {
|
|
769
|
+
view: understandMatch[1],
|
|
770
|
+
...(understandMatch[2] ? { intent: unescapeDoubleQuoted(understandMatch[2]) } : {}),
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (command === 'projscan session touched --format json') {
|
|
775
|
+
return {
|
|
776
|
+
tool: 'projscan_session',
|
|
777
|
+
args: { action: 'touched' },
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
function unescapeDoubleQuoted(value) {
|
|
783
|
+
return value.replace(/\\(["\\$`])/g, '$1');
|
|
784
|
+
}
|
|
785
|
+
function resumeChecklist(plan, cursor, inputBindings, followUps) {
|
|
786
|
+
const current = findStepInPlan(plan, cursor.stepId);
|
|
787
|
+
const currentItem = current
|
|
788
|
+
? resumeChecklistItemFromStep(current.phase, current.step, currentChecklistKind(current.step), cursor.stepId)
|
|
789
|
+
: undefined;
|
|
790
|
+
const includedStepIds = new Set(currentItem ? [currentItem.stepId] : []);
|
|
791
|
+
const inputItems = inputBindings.flatMap((binding) => {
|
|
792
|
+
if (includedStepIds.has(binding.inputId))
|
|
793
|
+
return [];
|
|
794
|
+
const found = findStepInPlan(plan, binding.inputId);
|
|
795
|
+
if (!found)
|
|
796
|
+
return [];
|
|
797
|
+
includedStepIds.add(found.step.id);
|
|
798
|
+
return [{
|
|
799
|
+
id: `resume-${found.step.id}`,
|
|
800
|
+
kind: 'resolve_input',
|
|
801
|
+
phaseId: found.phase.id,
|
|
802
|
+
stepId: found.step.id,
|
|
803
|
+
status: found.step.status,
|
|
804
|
+
label: binding.label,
|
|
805
|
+
placeholder: binding.placeholder,
|
|
806
|
+
instruction: binding.instruction,
|
|
807
|
+
followUpIds: binding.followUpIds,
|
|
808
|
+
...(found.step.dependsOn && found.step.dependsOn.length > 0 ? { dependsOn: found.step.dependsOn } : {}),
|
|
809
|
+
...(found.step.unlocks && found.step.unlocks.length > 0 ? { unlocks: found.step.unlocks } : {}),
|
|
810
|
+
}];
|
|
811
|
+
});
|
|
812
|
+
const followUpItems = followUps.flatMap((followUp) => {
|
|
813
|
+
if (includedStepIds.has(followUp.id))
|
|
814
|
+
return [];
|
|
815
|
+
includedStepIds.add(followUp.id);
|
|
816
|
+
return [{
|
|
817
|
+
id: `resume-${followUp.id}`,
|
|
818
|
+
kind: 'run_follow_up',
|
|
819
|
+
phaseId: followUp.phaseId,
|
|
820
|
+
stepId: followUp.id,
|
|
821
|
+
status: followUp.status,
|
|
822
|
+
label: followUp.label,
|
|
823
|
+
...(followUp.command ? { command: followUp.command } : {}),
|
|
824
|
+
...(followUp.tool ? { tool: followUp.tool } : {}),
|
|
825
|
+
...(followUp.args ? { args: followUp.args } : {}),
|
|
826
|
+
...(followUp.blockedBy && followUp.blockedBy.length > 0 ? { blockedBy: followUp.blockedBy } : {}),
|
|
827
|
+
...(followUp.dependsOn && followUp.dependsOn.length > 0 ? { dependsOn: followUp.dependsOn } : {}),
|
|
828
|
+
}];
|
|
829
|
+
});
|
|
830
|
+
const currentCommand = current?.step.command;
|
|
831
|
+
const proofItems = stepsForPhase(plan, 'proof')
|
|
832
|
+
.filter(({ step }) => step.command && step.command !== currentCommand)
|
|
833
|
+
.map(({ phase, step }) => resumeChecklistItemFromStep(phase, step, 'run_proof', step.id));
|
|
834
|
+
const doneItems = stepsForPhase(plan, 'done_when')
|
|
835
|
+
.map(({ phase, step }) => resumeChecklistItemFromStep(phase, step, 'confirm_done', step.id));
|
|
836
|
+
return [
|
|
837
|
+
...(currentItem ? [currentItem] : []),
|
|
838
|
+
...inputItems,
|
|
839
|
+
...followUpItems,
|
|
840
|
+
...proofItems,
|
|
841
|
+
...doneItems,
|
|
842
|
+
];
|
|
843
|
+
}
|
|
844
|
+
function resumeChecklistItemFromStep(phase, step, kind, stepId) {
|
|
845
|
+
return {
|
|
846
|
+
id: `resume-${stepId}`,
|
|
847
|
+
kind,
|
|
848
|
+
phaseId: phase.id,
|
|
849
|
+
stepId,
|
|
850
|
+
status: step.status,
|
|
851
|
+
label: step.label,
|
|
852
|
+
...(step.command ? { command: step.command } : {}),
|
|
853
|
+
...(step.tool ? { tool: step.tool } : {}),
|
|
854
|
+
...(step.args ? { args: step.args } : {}),
|
|
855
|
+
...(step.placeholder ? { placeholder: step.placeholder } : {}),
|
|
856
|
+
...(step.instruction ? { instruction: step.instruction } : {}),
|
|
857
|
+
...(step.blockedBy && step.blockedBy.length > 0 ? { blockedBy: step.blockedBy } : {}),
|
|
858
|
+
...(step.dependsOn && step.dependsOn.length > 0 ? { dependsOn: step.dependsOn } : {}),
|
|
859
|
+
...(step.unlocks && step.unlocks.length > 0 ? { unlocks: step.unlocks } : {}),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function currentChecklistKind(step) {
|
|
863
|
+
if (step.kind === 'input')
|
|
864
|
+
return 'resolve_input';
|
|
865
|
+
if (step.kind === 'proof')
|
|
866
|
+
return 'run_proof';
|
|
867
|
+
if (step.kind === 'criterion')
|
|
868
|
+
return 'confirm_done';
|
|
869
|
+
return 'run_current';
|
|
870
|
+
}
|
|
871
|
+
function resumeInputBindings(plan, cursor) {
|
|
872
|
+
const ids = uniqueStrings([
|
|
873
|
+
...(cursor.kind === 'input' ? [cursor.stepId] : []),
|
|
874
|
+
...(cursor.unlocks ?? []),
|
|
875
|
+
]);
|
|
876
|
+
return ids.flatMap((id) => {
|
|
877
|
+
const found = findStepInPlan(plan, id);
|
|
878
|
+
if (!found || found.step.kind !== 'input' || !found.step.placeholder || !found.step.instruction)
|
|
879
|
+
return [];
|
|
880
|
+
const followUpIds = (found.step.unlocks ?? []).filter((unlockedId) => findStepInPlan(plan, unlockedId)?.phase.id === 'follow_up');
|
|
881
|
+
return [{
|
|
882
|
+
inputId: found.step.id,
|
|
883
|
+
label: found.step.label,
|
|
884
|
+
placeholder: found.step.placeholder,
|
|
885
|
+
instruction: found.step.instruction,
|
|
886
|
+
followUpIds,
|
|
887
|
+
}];
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function resumeFollowUps(plan, cursor) {
|
|
891
|
+
const followUpIds = new Set();
|
|
892
|
+
for (const id of cursor.unlocks ?? []) {
|
|
893
|
+
const found = findStepInPlan(plan, id);
|
|
894
|
+
if (!found)
|
|
895
|
+
continue;
|
|
896
|
+
if (found.phase.id === 'follow_up')
|
|
897
|
+
followUpIds.add(found.step.id);
|
|
898
|
+
for (const unlockedId of found.step.unlocks ?? []) {
|
|
899
|
+
const unlocked = findStepInPlan(plan, unlockedId);
|
|
900
|
+
if (unlocked?.phase.id === 'follow_up')
|
|
901
|
+
followUpIds.add(unlocked.step.id);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return Array.from(followUpIds).flatMap((id) => {
|
|
905
|
+
const found = findStepInPlan(plan, id);
|
|
906
|
+
if (!found)
|
|
907
|
+
return [];
|
|
908
|
+
return [{
|
|
909
|
+
id: found.step.id,
|
|
910
|
+
phaseId: found.phase.id,
|
|
911
|
+
kind: found.step.kind,
|
|
912
|
+
status: found.step.status,
|
|
913
|
+
label: found.step.label,
|
|
914
|
+
...(found.step.command ? { command: found.step.command } : {}),
|
|
915
|
+
...(found.step.tool ? { tool: found.step.tool } : {}),
|
|
916
|
+
...(found.step.args ? { args: found.step.args } : {}),
|
|
917
|
+
...(found.step.blockedBy && found.step.blockedBy.length > 0 ? { blockedBy: found.step.blockedBy } : {}),
|
|
918
|
+
...(found.step.dependsOn && found.step.dependsOn.length > 0 ? { dependsOn: found.step.dependsOn } : {}),
|
|
919
|
+
}];
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
function resumeToolCall(plan, cursor) {
|
|
923
|
+
const found = findStepInPlan(plan, cursor.stepId);
|
|
924
|
+
if (!found?.step.tool || !argsAreReady(found.step.args))
|
|
925
|
+
return undefined;
|
|
926
|
+
return {
|
|
927
|
+
tool: found.step.tool,
|
|
928
|
+
...(typeof found.step.args !== 'undefined' ? { args: found.step.args } : {}),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function resolveResumeReferences(plan, ids) {
|
|
932
|
+
if (!ids || ids.length === 0)
|
|
933
|
+
return [];
|
|
934
|
+
const references = [];
|
|
935
|
+
for (const id of ids) {
|
|
936
|
+
const found = findStepInPlan(plan, id);
|
|
937
|
+
if (!found)
|
|
938
|
+
continue;
|
|
939
|
+
references.push({
|
|
940
|
+
id: found.step.id,
|
|
941
|
+
phaseId: found.phase.id,
|
|
942
|
+
kind: found.step.kind,
|
|
943
|
+
status: found.step.status,
|
|
944
|
+
label: found.step.label,
|
|
945
|
+
...(found.step.instruction ? { instruction: found.step.instruction } : {}),
|
|
946
|
+
...(found.step.command ? { command: found.step.command } : {}),
|
|
947
|
+
...(found.step.placeholder ? { placeholder: found.step.placeholder } : {}),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
return references;
|
|
951
|
+
}
|
|
952
|
+
function findStepInPlan(plan, id) {
|
|
953
|
+
for (const phase of plan.phases) {
|
|
954
|
+
for (const step of phase.steps) {
|
|
955
|
+
if (step.id === id)
|
|
956
|
+
return { phase, step };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
function stepsForPhase(plan, phaseId) {
|
|
962
|
+
const phase = plan.phases.find((item) => item.id === phaseId);
|
|
963
|
+
return phase ? phase.steps.map((step) => ({ phase, step })) : [];
|
|
964
|
+
}
|
|
965
|
+
function resumeUnlocksSentence(unlocks, rawIds) {
|
|
966
|
+
if (unlocks.length > 0)
|
|
967
|
+
return ` This can unlock ${unlocks.map(formatResumeReferenceLabel).join(', ')}.`;
|
|
968
|
+
return rawIds && rawIds.length > 0 ? ` This can unlock ${rawIds.join(', ')}.` : '';
|
|
969
|
+
}
|
|
970
|
+
function resumeBlockersSentence(blockedBy, rawIds) {
|
|
971
|
+
if (blockedBy.length > 0)
|
|
972
|
+
return ` Blocked by ${blockedBy.map(formatResumeReferenceLabel).join(', ')}.`;
|
|
973
|
+
return rawIds && rawIds.length > 0 ? ` Blocked by ${rawIds.join(', ')}.` : '';
|
|
974
|
+
}
|
|
975
|
+
function formatResumeReferenceLabel(reference) {
|
|
976
|
+
return `${reference.id} (${reference.label})`;
|
|
977
|
+
}
|
|
978
|
+
function renderRunbookResumeLines(resume) {
|
|
979
|
+
const lines = ['## Resume'];
|
|
980
|
+
if (resume.commandBlock) {
|
|
981
|
+
lines.push('Run now:', '```sh', resume.commandBlock, '```');
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
lines.push(`Do now: ${resume.instruction}`);
|
|
985
|
+
}
|
|
986
|
+
if (resume.toolCall) {
|
|
987
|
+
lines.push(`MCP call: ${formatRunbookToolCall(resume.toolCall)}`);
|
|
988
|
+
}
|
|
989
|
+
if (resume.unlocks && resume.unlocks.length > 0) {
|
|
990
|
+
lines.push('After running, resolve:', ...resume.unlocks.map((reference) => `- ${formatRunbookResumeReference(reference)}`));
|
|
991
|
+
}
|
|
992
|
+
if (resume.inputBindings && resume.inputBindings.length > 0) {
|
|
993
|
+
lines.push('Template inputs:', ...resume.inputBindings.map((binding) => `- ${formatRunbookInputBinding(binding)}`));
|
|
994
|
+
}
|
|
995
|
+
if (resume.checklist && resume.checklist.length > 0) {
|
|
996
|
+
lines.push('Resume checklist:', ...resume.checklist.map((item) => `- ${formatRunbookChecklistItem(item)}`));
|
|
997
|
+
}
|
|
998
|
+
if (resume.remainingProofItems && resume.remainingProofItems.length > 0) {
|
|
999
|
+
lines.push('Proof queue:', ...resume.remainingProofItems.map((item) => `- ${formatRunbookProofItem(item)}`));
|
|
1000
|
+
}
|
|
1001
|
+
if (resume.remainingProofCommands && resume.remainingProofCommands.length > 0) {
|
|
1002
|
+
lines.push('Remaining proof:', ...resume.remainingProofCommands.map((command) => `- \`${command}\``));
|
|
1003
|
+
}
|
|
1004
|
+
if (resume.remainingProofToolCalls && resume.remainingProofToolCalls.length > 0) {
|
|
1005
|
+
lines.push('MCP proof calls:', ...resume.remainingProofToolCalls.map((toolCall) => `- ${formatRunbookProofToolCall(toolCall)}`));
|
|
1006
|
+
}
|
|
1007
|
+
if (resume.followUps && resume.followUps.length > 0) {
|
|
1008
|
+
lines.push('Then use:', ...resume.followUps.map((followUp) => `- ${formatRunbookFollowUp(followUp)}`));
|
|
1009
|
+
}
|
|
1010
|
+
if (resume.blockedBy && resume.blockedBy.length > 0) {
|
|
1011
|
+
lines.push('Blocked by:', ...resume.blockedBy.map((reference) => `- ${formatRunbookResumeReference(reference)}`));
|
|
1012
|
+
}
|
|
1013
|
+
lines.push(`Prompt: ${resume.prompt}`);
|
|
1014
|
+
return lines;
|
|
1015
|
+
}
|
|
1016
|
+
function formatRunbookResumeReference(reference) {
|
|
1017
|
+
const detail = reference.instruction ?? reference.command ?? reference.label;
|
|
1018
|
+
return `${reference.id} (${reference.label}): ${detail}`;
|
|
1019
|
+
}
|
|
1020
|
+
function formatRunbookInputBinding(binding) {
|
|
1021
|
+
return `${binding.placeholder} -> ${binding.inputId} (${binding.label}): ${binding.instruction}`;
|
|
1022
|
+
}
|
|
1023
|
+
function formatRunbookChecklistItem(item) {
|
|
1024
|
+
const action = item.command
|
|
1025
|
+
?? (item.placeholder && item.instruction ? `${item.placeholder} -> ${item.instruction}` : undefined)
|
|
1026
|
+
?? item.instruction
|
|
1027
|
+
?? item.label;
|
|
1028
|
+
return `[${item.status}] ${item.kind} ${item.stepId}: ${action}${formatRunbookChecklistAnnotation(item)}`;
|
|
1029
|
+
}
|
|
1030
|
+
function formatRunbookChecklistAnnotation(item) {
|
|
1031
|
+
if (item.tool) {
|
|
1032
|
+
return ` (MCP: ${formatRunbookToolCall({ tool: item.tool, ...(typeof item.args !== 'undefined' ? { args: item.args } : {}) })})`;
|
|
1033
|
+
}
|
|
1034
|
+
if (item.kind === 'run_proof' && item.command)
|
|
1035
|
+
return ' (CLI only)';
|
|
1036
|
+
return '';
|
|
1037
|
+
}
|
|
1038
|
+
function formatRunbookToolCall(toolCall) {
|
|
1039
|
+
return typeof toolCall.args !== 'undefined'
|
|
1040
|
+
? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
|
|
1041
|
+
: toolCall.tool;
|
|
1042
|
+
}
|
|
1043
|
+
function formatRunbookProofToolCall(toolCall) {
|
|
1044
|
+
return `${toolCall.stepId}: ${formatRunbookToolCall(toolCall)}`;
|
|
1045
|
+
}
|
|
1046
|
+
function formatRunbookProofItem(item) {
|
|
1047
|
+
const proofAction = item.toolCall ? `MCP: ${formatRunbookToolCall(item.toolCall)}` : 'CLI only';
|
|
1048
|
+
return `${item.stepId}: \`${item.command}\` (${proofAction})`;
|
|
1049
|
+
}
|
|
1050
|
+
function formatRunbookFollowUp(followUp) {
|
|
1051
|
+
const action = followUp.command
|
|
1052
|
+
?? (followUp.tool ? formatRunbookToolCall({ tool: followUp.tool, ...(typeof followUp.args !== 'undefined' ? { args: followUp.args } : {}) }) : followUp.label);
|
|
1053
|
+
return `${followUp.id} (${followUp.label}): ${action}`;
|
|
1054
|
+
}
|
|
1055
|
+
function renderRunbookCursorLines(cursor) {
|
|
1056
|
+
const lines = [
|
|
1057
|
+
'## Current Cursor',
|
|
1058
|
+
`- Step: ${cursor.stepId} in ${cursor.phaseId}`,
|
|
1059
|
+
];
|
|
1060
|
+
if (cursor.command) {
|
|
1061
|
+
lines.push(`- Command: \`${cursor.command}\``);
|
|
1062
|
+
}
|
|
1063
|
+
else if (cursor.instruction) {
|
|
1064
|
+
lines.push(`- Input: ${cursor.instruction}`);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
lines.push(`- Label: ${cursor.label}`);
|
|
1068
|
+
}
|
|
1069
|
+
if (cursor.tool) {
|
|
1070
|
+
lines.push(`- MCP call: ${formatRunbookToolCall({ tool: cursor.tool, ...(typeof cursor.args !== 'undefined' ? { args: cursor.args } : {}) })}`);
|
|
1071
|
+
}
|
|
1072
|
+
if (cursor.blockedBy && cursor.blockedBy.length > 0) {
|
|
1073
|
+
lines.push(`- Blocked by: ${cursor.blockedBy.join(', ')}`);
|
|
1074
|
+
}
|
|
1075
|
+
if (cursor.unlocks && cursor.unlocks.length > 0) {
|
|
1076
|
+
lines.push(`- Unlocks: ${cursor.unlocks.join(', ')}`);
|
|
1077
|
+
}
|
|
1078
|
+
lines.push(`- Why: ${cursor.reason}`);
|
|
1079
|
+
return lines;
|
|
1080
|
+
}
|
|
1081
|
+
function buildMissionExecutionPlan(input) {
|
|
1082
|
+
const phases = [];
|
|
1083
|
+
const readyStepIds = input.readyActions.map((_, index) => `ready-${index + 1}`);
|
|
1084
|
+
const inputStepIdsByPlaceholder = new Map(input.unresolvedInputs.map((item, index) => [item.placeholder, `input-${index + 1}`]));
|
|
1085
|
+
const nextActionStep = actionToExecutionStep('next-action-1', input.primaryAction);
|
|
1086
|
+
phases.push({
|
|
1087
|
+
id: 'next_action',
|
|
1088
|
+
title: 'Next Action',
|
|
1089
|
+
status: nextActionStep.status,
|
|
1090
|
+
steps: [nextActionStep],
|
|
1091
|
+
});
|
|
1092
|
+
if (input.readyActions.length > 0) {
|
|
1093
|
+
phases.push({
|
|
1094
|
+
id: 'ready_now',
|
|
1095
|
+
title: 'Ready Commands',
|
|
1096
|
+
status: 'ready',
|
|
1097
|
+
steps: input.readyActions.map((action, index) => {
|
|
1098
|
+
const step = actionToExecutionStep(`ready-${index + 1}`, action);
|
|
1099
|
+
const unlockedInputs = Array.from(inputStepIdsByPlaceholder.values());
|
|
1100
|
+
if (index === 0 && unlockedInputs.length > 0)
|
|
1101
|
+
step.unlocks = unlockedInputs;
|
|
1102
|
+
return step;
|
|
1103
|
+
}),
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
if (input.unresolvedInputs.length > 0) {
|
|
1107
|
+
phases.push({
|
|
1108
|
+
id: 'resolve_inputs',
|
|
1109
|
+
title: 'Resolve Inputs',
|
|
1110
|
+
status: 'blocked',
|
|
1111
|
+
steps: input.unresolvedInputs.map((item, index) => {
|
|
1112
|
+
const id = `input-${index + 1}`;
|
|
1113
|
+
const followUps = followUpIdsForPlaceholder(input.actionPlan, item.placeholder);
|
|
1114
|
+
return {
|
|
1115
|
+
id,
|
|
1116
|
+
kind: 'input',
|
|
1117
|
+
status: 'blocked',
|
|
1118
|
+
label: item.name,
|
|
1119
|
+
...(readyStepIds[0] ? { dependsOn: [readyStepIds[0]] } : {}),
|
|
1120
|
+
...(followUps.length > 0 ? { unlocks: followUps } : {}),
|
|
1121
|
+
placeholder: item.placeholder,
|
|
1122
|
+
instruction: item.instruction,
|
|
1123
|
+
};
|
|
1124
|
+
}),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
const pendingActions = input.actionPlan.filter((action) => !isReadyAction(action));
|
|
1128
|
+
if (pendingActions.length > 0) {
|
|
1129
|
+
phases.push({
|
|
1130
|
+
id: 'follow_up',
|
|
1131
|
+
title: 'Follow Up',
|
|
1132
|
+
status: 'pending',
|
|
1133
|
+
steps: pendingActions.map((action, index) => {
|
|
1134
|
+
const step = actionToExecutionStep(`follow-up-${index + 1}`, action);
|
|
1135
|
+
const blockedBy = placeholdersInAction(action)
|
|
1136
|
+
.map((placeholder) => inputStepIdsByPlaceholder.get(placeholder))
|
|
1137
|
+
.filter((id) => typeof id === 'string');
|
|
1138
|
+
if (blockedBy.length > 0) {
|
|
1139
|
+
step.blockedBy = blockedBy;
|
|
1140
|
+
step.dependsOn = uniqueStrings([readyStepIds[0] ?? '', ...blockedBy].filter(Boolean));
|
|
1141
|
+
}
|
|
1142
|
+
return step;
|
|
1143
|
+
}),
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (input.proofCommands.length > 0) {
|
|
1147
|
+
phases.push({
|
|
1148
|
+
id: 'proof',
|
|
1149
|
+
title: 'Proof',
|
|
1150
|
+
status: 'ready',
|
|
1151
|
+
steps: input.proofCommands.map((command, index) => {
|
|
1152
|
+
const toolCall = proofCommandToolCall(command);
|
|
1153
|
+
return {
|
|
1154
|
+
id: `proof-${index + 1}`,
|
|
1155
|
+
kind: 'proof',
|
|
1156
|
+
status: 'ready',
|
|
1157
|
+
label: command,
|
|
1158
|
+
command,
|
|
1159
|
+
...(toolCall ? {
|
|
1160
|
+
tool: toolCall.tool,
|
|
1161
|
+
...(typeof toolCall.args !== 'undefined' ? { args: toolCall.args } : {}),
|
|
1162
|
+
} : {}),
|
|
1163
|
+
};
|
|
1164
|
+
}),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
phases.push({
|
|
1168
|
+
id: 'done_when',
|
|
1169
|
+
title: 'Done When',
|
|
1170
|
+
status: 'pending',
|
|
1171
|
+
steps: input.successCriteria.map((criterion, index) => ({
|
|
1172
|
+
id: `criterion-${index + 1}`,
|
|
1173
|
+
kind: 'criterion',
|
|
1174
|
+
status: 'pending',
|
|
1175
|
+
label: criterion,
|
|
1176
|
+
})),
|
|
1177
|
+
});
|
|
1178
|
+
const cursor = executionCursor(phases);
|
|
1179
|
+
return {
|
|
1180
|
+
summary: executionPlanSummary(input.readyActions.length, input.unresolvedInputs.length, input.proofCommands.length),
|
|
1181
|
+
currentPhase: cursor.phaseId,
|
|
1182
|
+
cursor,
|
|
1183
|
+
phases,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function actionToExecutionStep(id, action) {
|
|
1187
|
+
const step = {
|
|
1188
|
+
id,
|
|
1189
|
+
kind: 'tool',
|
|
1190
|
+
status: executionStatusForAction(action),
|
|
1191
|
+
label: action.label,
|
|
1192
|
+
};
|
|
1193
|
+
if (typeof action.command === 'string')
|
|
1194
|
+
step.command = action.command;
|
|
1195
|
+
if (typeof action.tool === 'string')
|
|
1196
|
+
step.tool = action.tool;
|
|
1197
|
+
if (action.args)
|
|
1198
|
+
step.args = action.args;
|
|
1199
|
+
return step;
|
|
1200
|
+
}
|
|
1201
|
+
function followUpIdsForPlaceholder(actionPlan, placeholder) {
|
|
1202
|
+
return actionPlan
|
|
1203
|
+
.filter((action) => !isReadyAction(action))
|
|
1204
|
+
.map((action, index) => ({ action, id: `follow-up-${index + 1}` }))
|
|
1205
|
+
.filter(({ action }) => placeholdersInAction(action).includes(placeholder))
|
|
1206
|
+
.map(({ id }) => id);
|
|
1207
|
+
}
|
|
1208
|
+
function placeholdersInAction(action) {
|
|
1209
|
+
const placeholders = new Set();
|
|
1210
|
+
if (typeof action.command === 'string') {
|
|
1211
|
+
for (const match of action.command.matchAll(/<[^<>]+>/g))
|
|
1212
|
+
placeholders.add(match[0]);
|
|
1213
|
+
}
|
|
1214
|
+
collectPlaceholdersFromValue(action.args, placeholders);
|
|
1215
|
+
return Array.from(placeholders);
|
|
1216
|
+
}
|
|
1217
|
+
function collectPlaceholdersFromValue(value, placeholders) {
|
|
1218
|
+
if (typeof value === 'string') {
|
|
1219
|
+
if (isPlaceholder(value))
|
|
1220
|
+
placeholders.add(value);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (Array.isArray(value)) {
|
|
1224
|
+
for (const item of value)
|
|
1225
|
+
collectPlaceholdersFromValue(item, placeholders);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (value && typeof value === 'object') {
|
|
1229
|
+
for (const item of Object.values(value))
|
|
1230
|
+
collectPlaceholdersFromValue(item, placeholders);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function executionStatusForAction(action) {
|
|
1234
|
+
if (isReadyAction(action))
|
|
1235
|
+
return 'ready';
|
|
1236
|
+
if ((typeof action.command === 'string' && !isRunnableCommand(action.command)) || !argsAreReady(action.args)) {
|
|
1237
|
+
return 'blocked';
|
|
1238
|
+
}
|
|
1239
|
+
return 'pending';
|
|
1240
|
+
}
|
|
1241
|
+
function executionCursor(phases) {
|
|
1242
|
+
const selected = findExecutionStep(phases, (phase, step) => phase.id === 'ready_now' && step.status === 'ready' && typeof step.command === 'string')
|
|
1243
|
+
?? findExecutionStep(phases, (phase, step) => phase.id === 'resolve_inputs' && step.status === 'blocked')
|
|
1244
|
+
?? findExecutionStep(phases, (phase, step) => phase.id === 'proof' && step.status === 'ready')
|
|
1245
|
+
?? findExecutionStep(phases, (phase) => phase.id === 'done_when')
|
|
1246
|
+
?? findExecutionStep(phases, (phase) => phase.id === 'next_action');
|
|
1247
|
+
if (!selected) {
|
|
1248
|
+
return {
|
|
1249
|
+
phaseId: 'done_when',
|
|
1250
|
+
stepId: 'criterion-1',
|
|
1251
|
+
status: 'pending',
|
|
1252
|
+
kind: 'criterion',
|
|
1253
|
+
label: 'The next action is complete and verified.',
|
|
1254
|
+
reason: 'Use this criterion to decide when the task is complete.',
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
phaseId: selected.phase.id,
|
|
1259
|
+
stepId: selected.step.id,
|
|
1260
|
+
status: selected.step.status,
|
|
1261
|
+
kind: selected.step.kind,
|
|
1262
|
+
label: selected.step.label,
|
|
1263
|
+
...(selected.step.command ? { command: selected.step.command } : {}),
|
|
1264
|
+
...(selected.step.tool ? { tool: selected.step.tool } : {}),
|
|
1265
|
+
...(typeof selected.step.args !== 'undefined' ? { args: selected.step.args } : {}),
|
|
1266
|
+
...(selected.step.instruction ? { instruction: selected.step.instruction } : {}),
|
|
1267
|
+
...(selected.step.placeholder ? { placeholder: selected.step.placeholder } : {}),
|
|
1268
|
+
...(selected.step.blockedBy && selected.step.blockedBy.length > 0 ? { blockedBy: selected.step.blockedBy } : {}),
|
|
1269
|
+
...(selected.step.unlocks && selected.step.unlocks.length > 0 ? { unlocks: selected.step.unlocks } : {}),
|
|
1270
|
+
reason: executionCursorReason(selected.step),
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function findExecutionStep(phases, predicate) {
|
|
1274
|
+
for (const phase of phases) {
|
|
1275
|
+
for (const step of phase.steps) {
|
|
1276
|
+
if (predicate(phase, step))
|
|
1277
|
+
return { phase, step };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return undefined;
|
|
1281
|
+
}
|
|
1282
|
+
function executionCursorReason(step) {
|
|
1283
|
+
if (step.status === 'ready' && step.kind === 'tool') {
|
|
1284
|
+
return step.unlocks && step.unlocks.length > 0
|
|
1285
|
+
? 'Run this ready command next; it can unlock later inputs or follow-up steps.'
|
|
1286
|
+
: 'Run this ready command next.';
|
|
1287
|
+
}
|
|
1288
|
+
if (step.status === 'blocked' && step.kind === 'input') {
|
|
1289
|
+
return 'Resolve this blocked input before running dependent follow-up steps.';
|
|
1290
|
+
}
|
|
1291
|
+
if (step.status === 'ready' && step.kind === 'proof') {
|
|
1292
|
+
return 'Run this proof command when action steps are complete.';
|
|
1293
|
+
}
|
|
1294
|
+
if (step.kind === 'criterion') {
|
|
1295
|
+
return 'Use this criterion to decide when the task is complete.';
|
|
1296
|
+
}
|
|
1297
|
+
return 'Use this step as the current execution pointer.';
|
|
1298
|
+
}
|
|
1299
|
+
function executionPlanSummary(readyCount, inputCount, proofCount) {
|
|
1300
|
+
const pieces = [`Run ${readyCount} ready ${pluralize(readyCount, 'step')}`];
|
|
1301
|
+
if (inputCount > 0)
|
|
1302
|
+
pieces.push(`resolve ${inputCount} input(s)`);
|
|
1303
|
+
if (proofCount > 0)
|
|
1304
|
+
pieces.push(`then gather ${proofCount} proof command(s)`);
|
|
1305
|
+
return `${pieces.join(', ')}.`;
|
|
1306
|
+
}
|
|
1307
|
+
function pluralize(count, singular) {
|
|
1308
|
+
return count === 1 ? singular : `${singular}s`;
|
|
1309
|
+
}
|
|
1310
|
+
function missionHandoff(currentStep, resume, nextAction, readyActions, needsInput, doneWhen, proofCommands, reviewGate) {
|
|
1311
|
+
const readyProofCommands = resume.remainingProofCommands ?? proofCommands;
|
|
1312
|
+
const readyProofToolCalls = resume.remainingProofToolCalls;
|
|
1313
|
+
const readyProofItems = resume.remainingProofItems;
|
|
1314
|
+
return {
|
|
1315
|
+
currentStep,
|
|
1316
|
+
resume,
|
|
1317
|
+
reviewGate,
|
|
1318
|
+
nextAction,
|
|
1319
|
+
readyActions,
|
|
1320
|
+
needsInput,
|
|
1321
|
+
doneWhen,
|
|
1322
|
+
readyProof: {
|
|
1323
|
+
summary: READY_PROOF_SUMMARY,
|
|
1324
|
+
commands: readyProofCommands,
|
|
1325
|
+
...(readyProofToolCalls && readyProofToolCalls.length > 0 ? { toolCalls: readyProofToolCalls } : {}),
|
|
1326
|
+
...(readyProofItems && readyProofItems.length > 0 ? { items: readyProofItems } : {}),
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function missionHandoffPrompt(resume, successCriteria, whyNow, unresolvedInputs, proofCommands, reviewGate) {
|
|
1331
|
+
const needsInput = unresolvedInputs.length > 0
|
|
1332
|
+
? ` Needs input: ${unresolvedInputs.map((input) => `${input.name}=${input.placeholder}`).join(', ')}.`
|
|
1333
|
+
: '';
|
|
1334
|
+
const proofCommandText = (resume.remainingProofCommands ?? proofCommands).slice(0, 3).join(' && ');
|
|
1335
|
+
const readyProof = proofCommandText.length > 0
|
|
1336
|
+
? ` Ready proof: ${READY_PROOF_SUMMARY} ${proofCommandText}.`
|
|
1337
|
+
: ` Ready proof: ${READY_PROOF_SUMMARY}.`;
|
|
1338
|
+
return `Resume: ${trimTrailingPunctuation(resume.prompt)}. Done when: ${trimTrailingPunctuation(successCriteria[0] ?? 'The proof commands pass')}.${needsInput} Why: ${whyNow}${readyProof}${handoffReviewGatePrompt(reviewGate)}`;
|
|
1339
|
+
}
|
|
1340
|
+
function handoffReviewGatePrompt(reviewGate) {
|
|
1341
|
+
const decisions = reviewGate.decisions
|
|
1342
|
+
.map((decision) => `${decision.label} => ${decision.reply}`)
|
|
1343
|
+
.join('; ');
|
|
1344
|
+
return ` Review gate: ${trimTrailingPunctuation(reviewGate.stopCondition)}. Reviewer replies: ${decisions}`;
|
|
1345
|
+
}
|
|
1346
|
+
function trimTrailingPunctuation(value) {
|
|
1347
|
+
return value.replace(/[.!?]+$/g, '');
|
|
1348
|
+
}
|
|
1349
|
+
function routesForIntent(intent) {
|
|
1350
|
+
if (!intent)
|
|
1351
|
+
return [];
|
|
1352
|
+
return routeIntent(intent).matches.map(routeEntryToStartIntent);
|
|
1353
|
+
}
|
|
1354
|
+
function routeEntryToStartIntent(entry) {
|
|
1355
|
+
return {
|
|
1356
|
+
intent: entry.intent,
|
|
1357
|
+
category: entry.category,
|
|
1358
|
+
tool: entry.tool,
|
|
1359
|
+
cli: entry.cli,
|
|
1360
|
+
why: entry.why,
|
|
1361
|
+
example: entry.example,
|
|
1362
|
+
confidence: entry.confidence,
|
|
1363
|
+
rank: entry.rank,
|
|
1364
|
+
score: entry.score,
|
|
1365
|
+
matchedKeywords: entry.matchedKeywords,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function routedWhyNow(route, actionPlan) {
|
|
1369
|
+
if (route.tool === 'projscan_impact' && actionPlan[0]?.tool === 'projscan_search') {
|
|
1370
|
+
return `Intent matched "${route.intent}", but the target is a phrase, so search first and then run ${route.tool} on the exact symbol or file.`;
|
|
1371
|
+
}
|
|
1372
|
+
if (route.tool === 'projscan_fix_suggest' && actionPlan[0]?.tool === 'projscan_doctor') {
|
|
1373
|
+
return `Intent matched "${route.intent}", but no issue id was named, so run projscan_doctor first and then run ${route.tool} on the selected issue.`;
|
|
1374
|
+
}
|
|
1375
|
+
if (route.tool === 'projscan_explain_issue' && actionPlan[0]?.tool === 'projscan_doctor') {
|
|
1376
|
+
return `Intent matched "${route.intent}", but no issue id was named, so run projscan_doctor first and then run ${route.tool} on the selected issue.`;
|
|
1377
|
+
}
|
|
1378
|
+
if (route.tool === 'projscan_upgrade' && actionPlan[0]?.tool === 'projscan_outdated') {
|
|
1379
|
+
return `Intent matched "${route.intent}", but no package was named, so run projscan_outdated first and then run ${route.tool} on the selected package.`;
|
|
1380
|
+
}
|
|
1381
|
+
return `Intent matched "${route.intent}", so start with ${route.tool} before broader workflow commands.`;
|
|
1382
|
+
}
|
|
1383
|
+
function missionStatus(setupOverall, verdict, adoptionGaps) {
|
|
1384
|
+
if (setupOverall === 'fail' || verdict === 'block')
|
|
1385
|
+
return 'blocked';
|
|
1386
|
+
if (adoptionGaps.some((gap) => gap.status === 'fail'))
|
|
1387
|
+
return 'needs_setup';
|
|
1388
|
+
if (setupOverall === 'warn' || verdict === 'caution' || adoptionGaps.some((gap) => gap.status === 'warn'))
|
|
1389
|
+
return 'needs_attention';
|
|
1390
|
+
return 'ready';
|
|
1391
|
+
}
|
|
1392
|
+
function actionFromRoute(intent, route) {
|
|
1393
|
+
const args = argsFromRouteIntent(intent, route);
|
|
1394
|
+
return {
|
|
1395
|
+
label: `Use ${route.tool} for ${intent}`,
|
|
1396
|
+
command: commandFromRouteIntent(intent, route, args),
|
|
1397
|
+
tool: route.tool,
|
|
1398
|
+
args,
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function missionActionPlan(intent, route, fixFirst, workplan, workflow) {
|
|
1402
|
+
if (route && intent)
|
|
1403
|
+
return actionPlanFromRoute(intent, route);
|
|
1404
|
+
const fallback = actionFromFixFirst(fixFirst) ?? actionFromWorkplan(workplan) ?? actionFromWorkflow(workflow);
|
|
1405
|
+
return [fallback];
|
|
1406
|
+
}
|
|
1407
|
+
function actionPlanFromRoute(intent, route) {
|
|
1408
|
+
if (route.tool === 'projscan_impact')
|
|
1409
|
+
return impactActionPlan(intent, route);
|
|
1410
|
+
if (route.tool === 'projscan_fix_suggest')
|
|
1411
|
+
return fixSuggestActionPlan(intent, route);
|
|
1412
|
+
if (route.tool === 'projscan_explain_issue')
|
|
1413
|
+
return explainIssueActionPlan(intent, route);
|
|
1414
|
+
if (route.tool === 'projscan_upgrade')
|
|
1415
|
+
return upgradeActionPlan(intent, route);
|
|
1416
|
+
if (route.tool === 'projscan_semantic_graph')
|
|
1417
|
+
return semanticGraphActionPlan(intent, route);
|
|
1418
|
+
if (route.tool === 'projscan_coupling')
|
|
1419
|
+
return couplingActionPlan(intent, route);
|
|
1420
|
+
if (route.tool === 'projscan_claim')
|
|
1421
|
+
return claimActionPlan(intent, route);
|
|
1422
|
+
return [actionFromRoute(intent, route)];
|
|
1423
|
+
}
|
|
1424
|
+
function impactActionPlan(intent, route) {
|
|
1425
|
+
const target = extractImpactTarget(intent) ?? extractFileTarget(intent);
|
|
1426
|
+
const impactLabel = `Use ${route.tool} for ${intent}`;
|
|
1427
|
+
if (target && isFilePathTarget(target)) {
|
|
1428
|
+
return [
|
|
1429
|
+
{
|
|
1430
|
+
label: impactLabel,
|
|
1431
|
+
command: `projscan impact ${quoteShellArg(target)} --format json`,
|
|
1432
|
+
tool: route.tool,
|
|
1433
|
+
args: { file: target },
|
|
1434
|
+
},
|
|
1435
|
+
];
|
|
1436
|
+
}
|
|
1437
|
+
if (target && isExactSymbolTarget(target)) {
|
|
1438
|
+
return [
|
|
1439
|
+
{
|
|
1440
|
+
label: impactLabel,
|
|
1441
|
+
command: `projscan impact --symbol ${target} --format json`,
|
|
1442
|
+
tool: route.tool,
|
|
1443
|
+
args: { symbol: target },
|
|
1444
|
+
},
|
|
1445
|
+
];
|
|
1446
|
+
}
|
|
1447
|
+
const searchQuery = target ?? intent;
|
|
1448
|
+
return [
|
|
1449
|
+
{
|
|
1450
|
+
label: 'Find exact target for impact analysis',
|
|
1451
|
+
command: `projscan search "${escapeDoubleQuoted(searchQuery)}" --format json`,
|
|
1452
|
+
tool: 'projscan_search',
|
|
1453
|
+
args: { query: searchQuery },
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
label: 'If search returns an exported symbol',
|
|
1457
|
+
command: 'projscan impact --symbol <symbol-from-search> --format json',
|
|
1458
|
+
tool: route.tool,
|
|
1459
|
+
args: { symbol: '<symbol-from-search>' },
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
label: 'If search returns a file path',
|
|
1463
|
+
command: 'projscan impact <file-from-search> --format json',
|
|
1464
|
+
tool: route.tool,
|
|
1465
|
+
args: { file: '<file-from-search>' },
|
|
1466
|
+
},
|
|
1467
|
+
];
|
|
1468
|
+
}
|
|
1469
|
+
function fixSuggestActionPlan(intent, route) {
|
|
1470
|
+
const issueId = extractIssueIdTarget(intent);
|
|
1471
|
+
if (issueId) {
|
|
1472
|
+
return [
|
|
1473
|
+
{
|
|
1474
|
+
label: `Use ${route.tool} for ${issueId}`,
|
|
1475
|
+
command: `projscan fix-suggest ${quoteShellArg(issueId)} --format json`,
|
|
1476
|
+
tool: route.tool,
|
|
1477
|
+
args: { issue_id: issueId },
|
|
1478
|
+
},
|
|
1479
|
+
];
|
|
1480
|
+
}
|
|
1481
|
+
return [
|
|
1482
|
+
{
|
|
1483
|
+
label: 'Find open issues before choosing a fix suggestion',
|
|
1484
|
+
command: 'projscan doctor --format json',
|
|
1485
|
+
tool: 'projscan_doctor',
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
label: 'Use fix-suggest for the selected issue',
|
|
1489
|
+
command: 'projscan fix-suggest <issue-id-from-doctor> --format json',
|
|
1490
|
+
tool: route.tool,
|
|
1491
|
+
args: { issue_id: '<issue-id-from-doctor>' },
|
|
1492
|
+
},
|
|
1493
|
+
];
|
|
1494
|
+
}
|
|
1495
|
+
function explainIssueActionPlan(intent, route) {
|
|
1496
|
+
const issueId = extractIssueIdTarget(intent);
|
|
1497
|
+
if (issueId) {
|
|
1498
|
+
return [
|
|
1499
|
+
{
|
|
1500
|
+
label: `Explain issue ${issueId}`,
|
|
1501
|
+
command: `projscan explain-issue ${quoteShellArg(issueId)} --format json`,
|
|
1502
|
+
tool: route.tool,
|
|
1503
|
+
args: { issue_id: issueId },
|
|
1504
|
+
},
|
|
1505
|
+
];
|
|
1506
|
+
}
|
|
1507
|
+
return [
|
|
1508
|
+
{
|
|
1509
|
+
label: 'Find open issues before explaining one',
|
|
1510
|
+
command: 'projscan doctor --format json',
|
|
1511
|
+
tool: 'projscan_doctor',
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
label: 'Explain the selected issue',
|
|
1515
|
+
command: 'projscan explain-issue <issue-id-from-doctor> --format json',
|
|
1516
|
+
tool: route.tool,
|
|
1517
|
+
args: { issue_id: '<issue-id-from-doctor>' },
|
|
1518
|
+
},
|
|
1519
|
+
];
|
|
1520
|
+
}
|
|
1521
|
+
function upgradeActionPlan(intent, route) {
|
|
1522
|
+
const packageName = extractPackageTarget(intent);
|
|
1523
|
+
if (packageName) {
|
|
1524
|
+
return [
|
|
1525
|
+
{
|
|
1526
|
+
label: `Preview upgrade impact for ${packageName}`,
|
|
1527
|
+
command: `projscan upgrade ${quoteShellArg(packageName)} --format json`,
|
|
1528
|
+
tool: route.tool,
|
|
1529
|
+
args: { package: packageName },
|
|
1530
|
+
},
|
|
1531
|
+
];
|
|
1532
|
+
}
|
|
1533
|
+
return [
|
|
1534
|
+
{
|
|
1535
|
+
label: 'Find package candidates before previewing an upgrade',
|
|
1536
|
+
command: 'projscan outdated --format json',
|
|
1537
|
+
tool: 'projscan_outdated',
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
label: 'Preview upgrade impact for the selected package',
|
|
1541
|
+
command: 'projscan upgrade <package-from-outdated> --format json',
|
|
1542
|
+
tool: route.tool,
|
|
1543
|
+
args: { package: '<package-from-outdated>' },
|
|
1544
|
+
},
|
|
1545
|
+
];
|
|
1546
|
+
}
|
|
1547
|
+
function semanticGraphActionPlan(intent, route) {
|
|
1548
|
+
const query = graphQueryFromIntent(intent);
|
|
1549
|
+
if (query && graphQueryIsReady(query)) {
|
|
1550
|
+
return [
|
|
1551
|
+
{
|
|
1552
|
+
label: `Run targeted graph query for ${intent}`,
|
|
1553
|
+
command: semanticGraphCommand(query),
|
|
1554
|
+
tool: route.tool,
|
|
1555
|
+
args: { query },
|
|
1556
|
+
},
|
|
1557
|
+
];
|
|
1558
|
+
}
|
|
1559
|
+
const fallback = { direction: query?.direction ?? 'importers', file: '<file-from-intent>' };
|
|
1560
|
+
return [
|
|
1561
|
+
{
|
|
1562
|
+
label: 'Find the file or symbol for the graph query',
|
|
1563
|
+
command: `projscan search "${escapeDoubleQuoted(extractSearchQuery(intent))}" --format json`,
|
|
1564
|
+
tool: 'projscan_search',
|
|
1565
|
+
args: { query: extractSearchQuery(intent) },
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
label: 'Run the targeted graph query',
|
|
1569
|
+
command: semanticGraphCommand(fallback),
|
|
1570
|
+
tool: route.tool,
|
|
1571
|
+
args: { query: fallback },
|
|
1572
|
+
},
|
|
1573
|
+
];
|
|
1574
|
+
}
|
|
1575
|
+
function couplingActionPlan(intent, route) {
|
|
1576
|
+
const direction = couplingDirectionFromIntent(intent);
|
|
1577
|
+
if (direction === 'cycles_only') {
|
|
1578
|
+
return [
|
|
1579
|
+
{
|
|
1580
|
+
label: 'Inspect circular import cycles',
|
|
1581
|
+
command: 'projscan coupling --cycles-only --format json',
|
|
1582
|
+
tool: route.tool,
|
|
1583
|
+
args: { direction },
|
|
1584
|
+
},
|
|
1585
|
+
];
|
|
1586
|
+
}
|
|
1587
|
+
if (direction === 'high_fan_in') {
|
|
1588
|
+
return [
|
|
1589
|
+
{
|
|
1590
|
+
label: 'Inspect high fan-in files',
|
|
1591
|
+
command: 'projscan coupling --high-fan-in --format json',
|
|
1592
|
+
tool: route.tool,
|
|
1593
|
+
args: { direction },
|
|
1594
|
+
},
|
|
1595
|
+
];
|
|
1596
|
+
}
|
|
1597
|
+
if (direction === 'high_fan_out') {
|
|
1598
|
+
return [
|
|
1599
|
+
{
|
|
1600
|
+
label: 'Inspect high fan-out files',
|
|
1601
|
+
command: 'projscan coupling --high-fan-out --format json',
|
|
1602
|
+
tool: route.tool,
|
|
1603
|
+
args: { direction },
|
|
1604
|
+
},
|
|
1605
|
+
];
|
|
1606
|
+
}
|
|
1607
|
+
return [
|
|
1608
|
+
{
|
|
1609
|
+
label: 'Inspect file coupling and instability',
|
|
1610
|
+
command: 'projscan coupling --format json',
|
|
1611
|
+
tool: route.tool,
|
|
1612
|
+
args: {},
|
|
1613
|
+
},
|
|
1614
|
+
];
|
|
1615
|
+
}
|
|
1616
|
+
function claimActionPlan(intent, route) {
|
|
1617
|
+
if (isClaimListIntent(intent))
|
|
1618
|
+
return [claimListAction(route)];
|
|
1619
|
+
const target = extractClaimTarget(intent) ?? '<target-from-intent>';
|
|
1620
|
+
const agent = extractClaimAgent(intent) ?? '<agent-name>';
|
|
1621
|
+
const addAction = claimAddAction(route, target, agent);
|
|
1622
|
+
if (!isPlaceholder(target) && !isPlaceholder(agent))
|
|
1623
|
+
return [addAction];
|
|
1624
|
+
return [
|
|
1625
|
+
{
|
|
1626
|
+
label: 'Review active claims before adding a file claim',
|
|
1627
|
+
command: 'projscan claim list --format json',
|
|
1628
|
+
tool: route.tool,
|
|
1629
|
+
args: { action: 'list' },
|
|
1630
|
+
},
|
|
1631
|
+
addAction,
|
|
1632
|
+
];
|
|
1633
|
+
}
|
|
1634
|
+
function claimListAction(route) {
|
|
1635
|
+
return {
|
|
1636
|
+
label: 'Review active claims',
|
|
1637
|
+
command: 'projscan claim list --format json',
|
|
1638
|
+
tool: route.tool,
|
|
1639
|
+
args: { action: 'list' },
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function claimAddAction(route, target, agent) {
|
|
1643
|
+
return {
|
|
1644
|
+
label: `Add claim for ${target}`,
|
|
1645
|
+
command: `projscan claim add ${quoteShellArgOrPlaceholder(target)} --agent ${quoteShellArgOrPlaceholder(agent)}`,
|
|
1646
|
+
tool: route.tool,
|
|
1647
|
+
args: { action: 'add', target, agent },
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function missionUnresolvedInputs(actionPlan) {
|
|
1651
|
+
const sourceAction = actionPlan[0]?.label ?? 'the previous action';
|
|
1652
|
+
const unresolved = [];
|
|
1653
|
+
for (const action of actionPlan.slice(1)) {
|
|
1654
|
+
if (!action.args)
|
|
1655
|
+
continue;
|
|
1656
|
+
for (const [name, value] of Object.entries(action.args)) {
|
|
1657
|
+
if (typeof value !== 'string' || !isPlaceholder(value))
|
|
1658
|
+
continue;
|
|
1659
|
+
unresolved.push({
|
|
1660
|
+
name,
|
|
1661
|
+
placeholder: value,
|
|
1662
|
+
sourceAction,
|
|
1663
|
+
instruction: placeholderInstruction(name, value),
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return dedupeUnresolvedInputs(unresolved);
|
|
1668
|
+
}
|
|
1669
|
+
function missionReadyActions(actionPlan) {
|
|
1670
|
+
return actionPlan.filter(isReadyAction);
|
|
1671
|
+
}
|
|
1672
|
+
function isReadyAction(action) {
|
|
1673
|
+
if (typeof action.command === 'string' && !isRunnableCommand(action.command))
|
|
1674
|
+
return false;
|
|
1675
|
+
if (!argsAreReady(action.args))
|
|
1676
|
+
return false;
|
|
1677
|
+
return typeof action.command === 'string' || typeof action.tool === 'string';
|
|
1678
|
+
}
|
|
1679
|
+
function argsAreReady(value) {
|
|
1680
|
+
if (typeof value === 'string')
|
|
1681
|
+
return !isPlaceholder(value);
|
|
1682
|
+
if (Array.isArray(value))
|
|
1683
|
+
return value.every(argsAreReady);
|
|
1684
|
+
if (value && typeof value === 'object')
|
|
1685
|
+
return Object.values(value).every(argsAreReady);
|
|
1686
|
+
return true;
|
|
1687
|
+
}
|
|
1688
|
+
function isPlaceholder(value) {
|
|
1689
|
+
return /^<[^<>]+>$/.test(value);
|
|
1690
|
+
}
|
|
1691
|
+
function placeholderInstruction(name, placeholder) {
|
|
1692
|
+
if (name === 'symbol')
|
|
1693
|
+
return `Replace ${placeholder} with an exported symbol returned by the search step.`;
|
|
1694
|
+
if (name === 'file')
|
|
1695
|
+
return `Replace ${placeholder} with a file path returned by the search step.`;
|
|
1696
|
+
if (name === 'issue_id')
|
|
1697
|
+
return `Replace ${placeholder} with an issue id from projscan doctor or projscan analyze.`;
|
|
1698
|
+
if (name === 'package')
|
|
1699
|
+
return `Replace ${placeholder} with a package name from projscan outdated or projscan dependencies.`;
|
|
1700
|
+
if (name === 'target')
|
|
1701
|
+
return `Replace ${placeholder} with the file, directory, or symbol to claim.`;
|
|
1702
|
+
if (name === 'agent')
|
|
1703
|
+
return `Replace ${placeholder} with the agent name holding the claim.`;
|
|
1704
|
+
return `Replace ${placeholder} with the ${name} value produced by the previous step.`;
|
|
1705
|
+
}
|
|
1706
|
+
function dedupeUnresolvedInputs(inputs) {
|
|
1707
|
+
const seen = new Set();
|
|
1708
|
+
const result = [];
|
|
1709
|
+
for (const input of inputs) {
|
|
1710
|
+
const key = `${input.name}:${input.placeholder}`;
|
|
1711
|
+
if (seen.has(key))
|
|
1712
|
+
continue;
|
|
1713
|
+
seen.add(key);
|
|
1714
|
+
result.push(input);
|
|
1715
|
+
}
|
|
1716
|
+
return result;
|
|
1717
|
+
}
|
|
1718
|
+
function argsFromRouteIntent(intent, route) {
|
|
1719
|
+
if (route.tool === 'projscan_privacy_check')
|
|
1720
|
+
return { offline: true };
|
|
1721
|
+
if (route.tool === 'projscan_preflight')
|
|
1722
|
+
return { mode: preflightModeFromIntent(intent) };
|
|
1723
|
+
if (route.tool === 'projscan_search')
|
|
1724
|
+
return { query: extractSearchQuery(intent) };
|
|
1725
|
+
if (route.tool === 'projscan_fix_suggest')
|
|
1726
|
+
return { issue_id: extractIssueIdTarget(intent) ?? '<issue-id-from-doctor>' };
|
|
1727
|
+
if (route.tool === 'projscan_explain_issue')
|
|
1728
|
+
return { issue_id: extractIssueIdTarget(intent) ?? '<issue-id-from-doctor>' };
|
|
1729
|
+
if (route.tool === 'projscan_upgrade')
|
|
1730
|
+
return { package: extractPackageTarget(intent) ?? '<package-from-outdated>' };
|
|
1731
|
+
if (route.tool === 'projscan_audit') {
|
|
1732
|
+
const packageName = extractAuditPackageTarget(intent);
|
|
1733
|
+
return packageName ? { package: packageName } : {};
|
|
1734
|
+
}
|
|
1735
|
+
if (route.tool === 'projscan_semantic_graph')
|
|
1736
|
+
return { query: graphQueryFromIntent(intent) ?? { direction: 'importers', file: '<file-from-intent>' } };
|
|
1737
|
+
if (route.tool === 'projscan_coupling') {
|
|
1738
|
+
const direction = couplingDirectionFromIntent(intent);
|
|
1739
|
+
return direction ? { direction } : {};
|
|
1740
|
+
}
|
|
1741
|
+
if (route.tool === 'projscan_file')
|
|
1742
|
+
return { file: extractFileTarget(intent) ?? '<file-from-intent>' };
|
|
1743
|
+
if (route.tool === 'projscan_understand') {
|
|
1744
|
+
const view = understandViewFromIntent(intent);
|
|
1745
|
+
return view === 'change' || view === 'verify' ? { view, intent } : { view };
|
|
1746
|
+
}
|
|
1747
|
+
if (route.tool === 'projscan_workplan')
|
|
1748
|
+
return { mode: 'before_edit' };
|
|
1749
|
+
if (route.tool === 'projscan_agent_brief')
|
|
1750
|
+
return { intent: agentBriefIntentFromIntent(intent) };
|
|
1751
|
+
if (route.tool === 'projscan_session')
|
|
1752
|
+
return { action: sessionActionFromIntent(intent) };
|
|
1753
|
+
if (route.tool === 'projscan_claim' && isClaimListIntent(intent))
|
|
1754
|
+
return { action: 'list' };
|
|
1755
|
+
if (route.tool === 'projscan_claim')
|
|
1756
|
+
return { action: 'add', target: extractClaimTarget(intent) ?? '<target-from-intent>', agent: extractClaimAgent(intent) ?? '<agent-name>' };
|
|
1757
|
+
if (route.tool === 'projscan_regression_plan')
|
|
1758
|
+
return { level: regressionLevelFromIntent(intent) };
|
|
1759
|
+
if (route.tool === 'projscan_evidence_pack')
|
|
1760
|
+
return { pr_comment: true };
|
|
1761
|
+
return {};
|
|
1762
|
+
}
|
|
1763
|
+
function commandFromRouteIntent(intent, route, args) {
|
|
1764
|
+
if (route.tool === 'projscan_privacy_check')
|
|
1765
|
+
return 'projscan privacy-check --offline';
|
|
1766
|
+
if (route.tool === 'projscan_preflight')
|
|
1767
|
+
return `projscan preflight --mode ${String(args.mode)} --format json`;
|
|
1768
|
+
if (route.tool === 'projscan_search')
|
|
1769
|
+
return `projscan search "${escapeDoubleQuoted(String(args.query))}" --format json`;
|
|
1770
|
+
if (route.tool === 'projscan_fix_suggest') {
|
|
1771
|
+
const issueId = String(args.issue_id);
|
|
1772
|
+
return `projscan fix-suggest ${isPlaceholder(issueId) ? issueId : quoteShellArg(issueId)} --format json`;
|
|
1773
|
+
}
|
|
1774
|
+
if (route.tool === 'projscan_explain_issue') {
|
|
1775
|
+
const issueId = String(args.issue_id);
|
|
1776
|
+
return `projscan explain-issue ${isPlaceholder(issueId) ? issueId : quoteShellArg(issueId)} --format json`;
|
|
1777
|
+
}
|
|
1778
|
+
if (route.tool === 'projscan_upgrade') {
|
|
1779
|
+
const packageName = String(args.package);
|
|
1780
|
+
return `projscan upgrade ${isPlaceholder(packageName) ? packageName : quoteShellArg(packageName)} --format json`;
|
|
1781
|
+
}
|
|
1782
|
+
if (route.tool === 'projscan_audit') {
|
|
1783
|
+
const packageName = typeof args.package === 'string' ? args.package : undefined;
|
|
1784
|
+
return packageName ? `projscan audit --package ${quoteShellArg(packageName)} --format json` : 'projscan audit --format json';
|
|
1785
|
+
}
|
|
1786
|
+
if (route.tool === 'projscan_semantic_graph')
|
|
1787
|
+
return semanticGraphCommand(args.query);
|
|
1788
|
+
if (route.tool === 'projscan_coupling')
|
|
1789
|
+
return couplingCommandFromArgs(args);
|
|
1790
|
+
if (route.tool === 'projscan_file')
|
|
1791
|
+
return `projscan file ${quoteShellArg(String(args.file))} --format json`;
|
|
1792
|
+
if (route.tool === 'projscan_understand') {
|
|
1793
|
+
const view = String(args.view);
|
|
1794
|
+
const routedIntent = typeof args.intent === 'string' ? ` --intent ${quoteShellArg(args.intent)}` : '';
|
|
1795
|
+
return `projscan understand --view ${view}${routedIntent} --format json`;
|
|
1796
|
+
}
|
|
1797
|
+
if (route.tool === 'projscan_workplan')
|
|
1798
|
+
return 'projscan workplan --mode before_edit --format json';
|
|
1799
|
+
if (route.tool === 'projscan_agent_brief')
|
|
1800
|
+
return `projscan agent-brief --intent ${String(args.intent)} --format json`;
|
|
1801
|
+
if (route.tool === 'projscan_session')
|
|
1802
|
+
return sessionCommandFromAction(String(args.action));
|
|
1803
|
+
if (route.tool === 'projscan_claim')
|
|
1804
|
+
return claimCommandFromArgs(args);
|
|
1805
|
+
if (route.tool === 'projscan_regression_plan')
|
|
1806
|
+
return `projscan regression-plan --level ${String(args.level)} --format json`;
|
|
1807
|
+
if (route.tool === 'projscan_evidence_pack')
|
|
1808
|
+
return 'projscan evidence-pack --pr-comment';
|
|
1809
|
+
return route.example;
|
|
1810
|
+
}
|
|
1811
|
+
function claimCommandFromArgs(args) {
|
|
1812
|
+
const action = String(args.action ?? 'list');
|
|
1813
|
+
if (action === 'add') {
|
|
1814
|
+
const target = String(args.target ?? '<target-from-intent>');
|
|
1815
|
+
const agent = String(args.agent ?? '<agent-name>');
|
|
1816
|
+
return `projscan claim add ${quoteShellArgOrPlaceholder(target)} --agent ${quoteShellArgOrPlaceholder(agent)}`;
|
|
1817
|
+
}
|
|
1818
|
+
return 'projscan claim list --format json';
|
|
1819
|
+
}
|
|
1820
|
+
function preflightModeFromIntent(intent) {
|
|
1821
|
+
const text = intent.toLowerCase();
|
|
1822
|
+
if (/\b(?:merge|merged|merging|release|rebase|rebasing|conflict|conflicts|resolve|resolving)\b/.test(text))
|
|
1823
|
+
return 'before_merge';
|
|
1824
|
+
if (/\bcommit|committing|committed|pr|pull\s+request\b/.test(text))
|
|
1825
|
+
return 'before_commit';
|
|
1826
|
+
return 'before_edit';
|
|
1827
|
+
}
|
|
1828
|
+
function regressionModeFromIntent(intent) {
|
|
1829
|
+
return /\bmerge|merged|merging|release\b/i.test(intent) ? 'before_merge' : 'before_commit';
|
|
1830
|
+
}
|
|
1831
|
+
function reviewModeFromIntent(intent) {
|
|
1832
|
+
return /\bmerge|merged|merging\b/i.test(intent) ? 'before_merge' : 'before_commit';
|
|
1833
|
+
}
|
|
1834
|
+
function regressionLevelFromIntent(intent) {
|
|
1835
|
+
const text = intent.toLowerCase();
|
|
1836
|
+
if (/\b(?:smoke|quick|minimum|minimal)\b/.test(text))
|
|
1837
|
+
return 'smoke';
|
|
1838
|
+
if (/\b(?:full|complete|comprehensive|exhaustive|release-grade)\b/.test(text))
|
|
1839
|
+
return 'full';
|
|
1840
|
+
return 'focused';
|
|
1841
|
+
}
|
|
1842
|
+
function agentBriefIntentFromIntent(intent) {
|
|
1843
|
+
const text = intent.toLowerCase();
|
|
1844
|
+
if (/\b(?:bug|bugs|fix|hunt)\b/.test(text))
|
|
1845
|
+
return 'bug_hunt';
|
|
1846
|
+
if (/\b(?:release|ship|shipping|publish)\b/.test(text))
|
|
1847
|
+
return 'release';
|
|
1848
|
+
if (/\b(?:refactor|cleanup|simplify)\b/.test(text))
|
|
1849
|
+
return 'refactor';
|
|
1850
|
+
if (/\b(?:hardening|security|dataflow|taint|injection)\b/.test(text))
|
|
1851
|
+
return 'hardening';
|
|
1852
|
+
return 'next_agent';
|
|
1853
|
+
}
|
|
1854
|
+
function sessionActionFromIntent(intent) {
|
|
1855
|
+
const text = intent.toLowerCase();
|
|
1856
|
+
if (/\b(?:event|events|history|log|logs|timeline)\b/.test(text))
|
|
1857
|
+
return 'events';
|
|
1858
|
+
if (/\b(?:current|summary|status)\b/.test(text))
|
|
1859
|
+
return 'current';
|
|
1860
|
+
return 'touched';
|
|
1861
|
+
}
|
|
1862
|
+
function sessionCommandFromAction(action) {
|
|
1863
|
+
if (action === 'events')
|
|
1864
|
+
return 'projscan session events --format json';
|
|
1865
|
+
if (action === 'current')
|
|
1866
|
+
return 'projscan session --format json';
|
|
1867
|
+
return 'projscan session touched --format json';
|
|
1868
|
+
}
|
|
1869
|
+
function couplingDirectionFromIntent(intent) {
|
|
1870
|
+
const text = intent.toLowerCase();
|
|
1871
|
+
if (/\b(?:circular|cycle|cycles)\b/.test(text))
|
|
1872
|
+
return 'cycles_only';
|
|
1873
|
+
if (/\bfan[-\s]?in\b|\bdepended\s+on\b|\bmost\s+depended\b/.test(text))
|
|
1874
|
+
return 'high_fan_in';
|
|
1875
|
+
if (/\bfan[-\s]?out\b/.test(text))
|
|
1876
|
+
return 'high_fan_out';
|
|
1877
|
+
return undefined;
|
|
1878
|
+
}
|
|
1879
|
+
function couplingCommandFromArgs(args) {
|
|
1880
|
+
const direction = typeof args.direction === 'string' ? args.direction : 'all';
|
|
1881
|
+
if (direction === 'cycles_only')
|
|
1882
|
+
return 'projscan coupling --cycles-only --format json';
|
|
1883
|
+
if (direction === 'high_fan_in')
|
|
1884
|
+
return 'projscan coupling --high-fan-in --format json';
|
|
1885
|
+
if (direction === 'high_fan_out')
|
|
1886
|
+
return 'projscan coupling --high-fan-out --format json';
|
|
1887
|
+
return 'projscan coupling --format json';
|
|
1888
|
+
}
|
|
1889
|
+
function isClaimListIntent(intent) {
|
|
1890
|
+
const text = intent.toLowerCase();
|
|
1891
|
+
return /\b(?:show|list|view|active)\b/.test(text) && /\b(?:claim|claims|lease|leases)\b/.test(text);
|
|
1892
|
+
}
|
|
1893
|
+
function understandViewFromIntent(intent) {
|
|
1894
|
+
const text = intent.toLowerCase();
|
|
1895
|
+
if (isPackageScriptDiscoveryIntent(text))
|
|
1896
|
+
return 'contracts';
|
|
1897
|
+
if (/\bnpm\s+scripts?\b|\bscripts?\s+(?:exist|available|defined|configured)\b/.test(text))
|
|
1898
|
+
return 'contracts';
|
|
1899
|
+
if (isLocalServiceSetupIntent(text))
|
|
1900
|
+
return 'contracts';
|
|
1901
|
+
if (/\b(?:seed|seeds|reset|resets|migrate|migrates|run|runs)\b.*\b(?:database|db|migrations?)\b|\b(?:database|db|migrations?)\b.*\b(?:seed|seeds|reset|resets|migrate|migrates|run|runs|command)\b/.test(text))
|
|
1902
|
+
return 'contracts';
|
|
1903
|
+
if (/\b(?:contract|contracts|public\s+api|public\s+apis|public\s+exports?|api\s+surface|deprecat(?:e|es|ed|ion|ing)|compatibility|compatible|env(?:ironment)?\s+vars?|env(?:ironment)?\s+variables?|config|configuration)\b/.test(text))
|
|
1904
|
+
return 'contracts';
|
|
1905
|
+
if (/\b(?:flow|flows|runtime|request\s+path|execution\s+path)\b/.test(text))
|
|
1906
|
+
return 'flow';
|
|
1907
|
+
if (isVerificationPlanningIntent(text))
|
|
1908
|
+
return 'verify';
|
|
1909
|
+
if (/\b(?:verify|verification|proof|test\s+plan|checks?)\b/.test(text))
|
|
1910
|
+
return 'verify';
|
|
1911
|
+
if (/\b(?:change|readiness|before\s+changing|before\s+rename|feature|endpoint|button|where\s+should\s+i\s+(?:put|add)|files\s+do\s+i\s+need\s+to\s+change|add|implement|build|create|wire|route|component|page|screen|view|webhook|login|checkout|migration|migrations|database|db|schema|table|column)\b/.test(text))
|
|
1912
|
+
return 'change';
|
|
1913
|
+
return 'map';
|
|
1914
|
+
}
|
|
1915
|
+
function isVerificationPlanningIntent(text) {
|
|
1916
|
+
if (/\b(?:smoke|focused|full|regression|fail|failing|failed|failure|failures|error|errors|broken|debug|flake|flaky|slow|slower|reproduce|quarantine)\b/.test(text)) {
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
const proofSelectionSignal = /\b(?:run|should|need|needs|must|before|push|pushing|prove|proof|verify|verification|checks?)\b/.test(text);
|
|
1920
|
+
if (!proofSelectionSignal && /\b(?:which|what|where|find|locate|search)\b.*\b(?:tests?|specs?)\b.*\b(?:cover|covers|covering|for)\b/.test(text)) {
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
if (/\b(?:coverage|scariest|untested|uncovered|gap|gaps|missing\s+tests?|no\s+tests?)\b/.test(text)) {
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
const testSubject = /\b(?:tests?|specs?|e2e|unit|integration|lint|typecheck|typechecking|build)\b/.test(text);
|
|
1927
|
+
const proofSignal = /\b(?:verify|verification|proof|prove|checks?)\b/.test(text);
|
|
1928
|
+
const gateSignal = /\b(?:before|push|pushing|commit|committing|review|merge|pr)\b/.test(text);
|
|
1929
|
+
const shouldSignal = /\b(?:should|need|needs|must)\b/.test(text);
|
|
1930
|
+
return (testSubject && (shouldSignal || gateSignal || proofSignal)) || (proofSignal && gateSignal);
|
|
1931
|
+
}
|
|
1932
|
+
function isPackageScriptDiscoveryIntent(text) {
|
|
1933
|
+
if (/\b(?:fail|failing|failed|failure|failures|error|errors|broken|debug|flake|flaky|slow|rerun|reproduce|quarantine)\b/.test(text)) {
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
const scriptTarget = /\b(?:tests?|e2e|unit|integration|storybook|cypress|playwright|eslint|prettier|format|lint|typecheck|typechecking|build)\b/.test(text);
|
|
1937
|
+
const scriptSubject = /\b(?:scripts?|commands?)\b/.test(text);
|
|
1938
|
+
const runSignal = /\b(?:run|runs|start)\b/.test(text);
|
|
1939
|
+
const directScriptTarget = /\b(?:e2e|storybook|cypress|playwright|eslint|prettier|format|lint|typecheck|typechecking|build)\b/.test(text);
|
|
1940
|
+
return (/\b(?:npm|package)\s+scripts?\b/.test(text) ||
|
|
1941
|
+
(scriptTarget && scriptSubject) ||
|
|
1942
|
+
(directScriptTarget && runSignal && !/\bshould\b/.test(text)));
|
|
1943
|
+
}
|
|
1944
|
+
function isLocalServiceSetupIntent(text) {
|
|
1945
|
+
if (/\b(?:fail|failing|failed|failure|failures|error|errors|broken|connection\s+refused|port|eaddrinuse|permission\s+denied|enoent|eresolve|peer)\b/.test(text)) {
|
|
1946
|
+
return false;
|
|
1947
|
+
}
|
|
1948
|
+
const action = /\b(?:run|runs|start|starts|command|commands|setup|set\s+up)\b/.test(text);
|
|
1949
|
+
const localServices = /\b(?:local|locally|dev)\b.*\bservices?\b|\bservices?\b.*\b(?:local|locally|dev)\b/.test(text);
|
|
1950
|
+
const dockerCompose = /\bdocker\s+compose\b/.test(text);
|
|
1951
|
+
return action && (localServices || dockerCompose);
|
|
1952
|
+
}
|
|
1953
|
+
function extractSearchQuery(intent) {
|
|
1954
|
+
const trimmed = intent.trim();
|
|
1955
|
+
const file = extractFileTarget(trimmed);
|
|
1956
|
+
if (file && /\b(?:where|find|locate|search)\b/i.test(trimmed) && /\btests?\b/i.test(trimmed)) {
|
|
1957
|
+
return `tests for ${file}`;
|
|
1958
|
+
}
|
|
1959
|
+
const envVar = extractEnvVarTarget(trimmed);
|
|
1960
|
+
if (envVar && /\b(?:where|find|locate|search|lookup|used|referenced|process)\b/i.test(trimmed)) {
|
|
1961
|
+
return envVar;
|
|
1962
|
+
}
|
|
1963
|
+
const envControl = trimmed.match(/\b(?:which|what|where|find|locate|search(?:\s+for)?|lookup)\s+(?:env(?:ironment)?\s+)?(?:var|vars|variable|variables)\s+(?:controls?|configures?|sets?|for)\s+(.+?)\s*[?!.]*$/i);
|
|
1964
|
+
if (envControl?.[1])
|
|
1965
|
+
return `${unwrapTarget(envControl[1].trim())} env var`;
|
|
1966
|
+
const quotedDebugText = extractQuotedTextTarget(trimmed);
|
|
1967
|
+
if (quotedDebugText && /\b(?:error|errors|message|messages|throws?|thrown|logs?|logged|logging)\b/i.test(trimmed)) {
|
|
1968
|
+
return quotedDebugText;
|
|
1969
|
+
}
|
|
1970
|
+
const observability = extractObservabilityQuery(trimmed);
|
|
1971
|
+
if (observability)
|
|
1972
|
+
return observability;
|
|
1973
|
+
const backgroundWork = extractBackgroundWorkQuery(trimmed);
|
|
1974
|
+
if (backgroundWork)
|
|
1975
|
+
return backgroundWork;
|
|
1976
|
+
const testData = extractTestDataQuery(trimmed);
|
|
1977
|
+
if (testData)
|
|
1978
|
+
return testData;
|
|
1979
|
+
const authorization = extractAuthorizationQuery(trimmed);
|
|
1980
|
+
if (authorization)
|
|
1981
|
+
return authorization;
|
|
1982
|
+
const reliability = extractReliabilityQuery(trimmed);
|
|
1983
|
+
if (reliability)
|
|
1984
|
+
return reliability;
|
|
1985
|
+
const dataContract = extractDataContractQuery(trimmed);
|
|
1986
|
+
if (dataContract)
|
|
1987
|
+
return dataContract;
|
|
1988
|
+
const uiInteraction = extractUiInteractionQuery(trimmed);
|
|
1989
|
+
if (uiInteraction)
|
|
1990
|
+
return uiInteraction;
|
|
1991
|
+
const styleSystem = extractStyleSystemQuery(trimmed);
|
|
1992
|
+
if (styleSystem)
|
|
1993
|
+
return styleSystem;
|
|
1994
|
+
const navigationLayout = extractNavigationLayoutQuery(trimmed);
|
|
1995
|
+
if (navigationLayout)
|
|
1996
|
+
return navigationLayout;
|
|
1997
|
+
const frontendPageRoute = extractFrontendPageRouteQuery(trimmed);
|
|
1998
|
+
if (frontendPageRoute)
|
|
1999
|
+
return frontendPageRoute;
|
|
2000
|
+
const stateManagement = extractStateManagementQuery(trimmed);
|
|
2001
|
+
if (stateManagement)
|
|
2002
|
+
return stateManagement;
|
|
2003
|
+
const dataAccess = extractDataAccessQuery(trimmed);
|
|
2004
|
+
if (dataAccess)
|
|
2005
|
+
return dataAccess;
|
|
2006
|
+
const integration = extractIntegrationQuery(trimmed);
|
|
2007
|
+
if (integration)
|
|
2008
|
+
return integration;
|
|
2009
|
+
const apiContract = extractApiContractQuery(trimmed);
|
|
2010
|
+
if (apiContract)
|
|
2011
|
+
return apiContract;
|
|
2012
|
+
const infraArtifact = extractInfraArtifactQuery(trimmed);
|
|
2013
|
+
if (infraArtifact)
|
|
2014
|
+
return infraArtifact;
|
|
2015
|
+
const communicationArtifact = extractCommunicationArtifactQuery(trimmed);
|
|
2016
|
+
if (communicationArtifact)
|
|
2017
|
+
return communicationArtifact;
|
|
2018
|
+
const domainWorkflow = extractDomainWorkflowQuery(trimmed);
|
|
2019
|
+
if (domainWorkflow)
|
|
2020
|
+
return domainWorkflow;
|
|
2021
|
+
const testCoverageLookup = trimmed.match(/\b(?:which|what|find|locate|search(?:\s+for)?|where\s+(?:are|is))\s+(?:the\s+)?(?:tests?|specs?)\s+(?:that\s+)?(?:cover|covers|covering)\s+(.+?)\s*[?!.]*$/i);
|
|
2022
|
+
if (testCoverageLookup?.[1])
|
|
2023
|
+
return `tests for ${unwrapTarget(testCoverageLookup[1].trim())}`;
|
|
2024
|
+
const testLocation = trimmed.match(/\b(?:where\s+(?:are|is)\s+|find\s+|locate\s+|search\s+(?:for\s+)?|lookup\s+)?(?:the\s+)?(?:tests?|specs?)\s+(?:for|of)\s+(.+?)\s*[?!.]*$/i);
|
|
2025
|
+
if (testLocation?.[1])
|
|
2026
|
+
return `tests for ${unwrapTarget(testLocation[1].trim())}`;
|
|
2027
|
+
const routePath = trimmed.match(/(?:^|\s)((?:(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+)?\/[A-Za-z0-9_./:{}-]+)/i);
|
|
2028
|
+
if (routePath?.[1] && /\b(?:handler|handles?|handled|route|routes|endpoint|endpoints|where|find|locate|search)\b/i.test(trimmed)) {
|
|
2029
|
+
return routePath[1].trim();
|
|
2030
|
+
}
|
|
2031
|
+
const codeHandler = trimmed.match(/\b(?:what|which)\s+(?:code|file|files)?\s*(?:handles?|contains|loads?|parses?|configures?|creates?)\s+(.+?)\s*[?!.]*$/i);
|
|
2032
|
+
if (codeHandler?.[1])
|
|
2033
|
+
return unwrapTarget(codeHandler[1].trim());
|
|
2034
|
+
const featureFlags = trimmed.match(/\b(?:which|what|where|find|locate|search(?:\s+for)?|lookup)\s+(?:are\s+|is\s+|do\s+|does\s+)?(?:the\s+)?((?:feature\s+)?flags?)\s+(?:exist|exists|configured|loaded|defined)?\s*[?!.]*$/i);
|
|
2035
|
+
if (featureFlags?.[1])
|
|
2036
|
+
return featureFlags[1].toLowerCase().includes('feature') ? 'feature flags' : 'flags';
|
|
2037
|
+
const migrationLookup = trimmed.match(/\b(?:where\s+(?:are|is)|which|what|find|locate|search(?:\s+for)?|lookup|show)\s+(?:me\s+)?(?:the\s+)?((?:database\s+|prisma\s+)?migrations?|(?:database\s+)?migration\s+files?)\s*(?:exist|exists|ran|located|live)?\s*[?!.]*$/i);
|
|
2038
|
+
if (migrationLookup?.[1]) {
|
|
2039
|
+
const target = migrationLookup[1].trim().toLowerCase();
|
|
2040
|
+
if (target.includes('prisma'))
|
|
2041
|
+
return 'Prisma migrations';
|
|
2042
|
+
if (target.includes('database'))
|
|
2043
|
+
return target.includes('file') ? 'database migration files' : 'database migrations';
|
|
2044
|
+
return target.includes('file') ? 'migration files' : 'migrations';
|
|
2045
|
+
}
|
|
2046
|
+
const generatedLookup = trimmed.match(/\b(?:show|find|locate|search(?:\s+for)?|where\s+(?:are|is)|which|what|is)\s+(?:me\s+)?(?:this\s+)?(?:the\s+)?(.+?)\s*[?!.]*$/i);
|
|
2047
|
+
if (generatedLookup?.[1] && /\bgenerated\b/i.test(generatedLookup[1])) {
|
|
2048
|
+
if (/\bfiles?\b/i.test(generatedLookup[1]))
|
|
2049
|
+
return 'generated files';
|
|
2050
|
+
if (/\bcode\b/i.test(generatedLookup[1]))
|
|
2051
|
+
return 'generated code';
|
|
2052
|
+
}
|
|
2053
|
+
const toolingConfig = extractToolingConfigQuery(trimmed);
|
|
2054
|
+
if (toolingConfig)
|
|
2055
|
+
return toolingConfig;
|
|
2056
|
+
const configDefinitionLookup = trimmed.match(/\bwhich\s+(?:config(?:uration)?\s+files?|files?)\s+(?:defines?|contains|sets?|configures?)\s+(.+?)\s*[?!.]*$/i);
|
|
2057
|
+
if (configDefinitionLookup?.[1] && /\bconfig(?:uration)?\b/i.test(trimmed))
|
|
2058
|
+
return `${unwrapTarget(configDefinitionLookup[1].trim())} config`;
|
|
2059
|
+
const configLookup = trimmed.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup|show)\s+(?:the\s+)?(.+?\bconfig(?:uration)?(?:\s+files?)?)\s*[?!.]*$/i);
|
|
2060
|
+
if (configLookup?.[1])
|
|
2061
|
+
return unwrapTarget(configLookup[1].trim());
|
|
2062
|
+
const ownership = trimmed.match(/\b(?:who|which\s+team)\s+owns?\s+(.+?)\s*[?!.]*$/i);
|
|
2063
|
+
if (ownership?.[1])
|
|
2064
|
+
return unwrapTarget(ownership[1].trim());
|
|
2065
|
+
const ownershipHelp = trimmed.match(/\bwho\s+(?:should\s+i\s+ask|can\s+help|knows|is\s+(?:the\s+)?(?:expert|contact))\s*(?:about|with|for)?\s+(.+?)\s*[?!.]*$/i);
|
|
2066
|
+
if (ownershipHelp?.[1])
|
|
2067
|
+
return unwrapTarget(ownershipHelp[1].trim());
|
|
2068
|
+
const expertLookup = trimmed.match(/\b(?:find|locate|search(?:\s+for)?|lookup)\s+(?:an?\s+)?(?:expert|experts|contact|contacts)\s+(?:for|on|about|with)\s+(.+?)\s*[?!.]*$/i);
|
|
2069
|
+
if (expertLookup?.[1])
|
|
2070
|
+
return unwrapTarget(expertLookup[1].trim());
|
|
2071
|
+
const codeOwners = trimmed.match(/\b(?:find|locate|search(?:\s+for)?|lookup)\s+(?:code\s+)?owners?\s+(?:for|of)\s+(.+?)\s*[?!.]*$/i);
|
|
2072
|
+
if (codeOwners?.[1])
|
|
2073
|
+
return unwrapTarget(codeOwners[1].trim());
|
|
2074
|
+
const whereImplemented = trimmed.match(/\bwhere\s+(?:is|are|do|does|we)?\s*(.+?)\s+(?:implemented|handled|configured|created|defined|loaded|parsed|documented)\b/i);
|
|
2075
|
+
if (whereImplemented?.[1])
|
|
2076
|
+
return unwrapTarget(whereImplemented[1].trim());
|
|
2077
|
+
const match = trimmed.match(/\b(?:search|find|locate|lookup)\s+(?:for\s+)?(.+)$/i);
|
|
2078
|
+
return unwrapTarget((match?.[1] ?? trimmed).trim());
|
|
2079
|
+
}
|
|
2080
|
+
function extractImpactTarget(intent) {
|
|
2081
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2082
|
+
const usageMatch = compactIntent.match(/\bwhere\s+(?:is|are)\s+[`'"]?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)[`'"]?\s+(?:used|referenced|called)\b/i);
|
|
2083
|
+
if (usageMatch?.[1] && !isGenericReferenceTarget(usageMatch[1]))
|
|
2084
|
+
return usageMatch[1];
|
|
2085
|
+
const match = compactIntent.match(/\b(?:rename|change|modify|delete|remove)\s+(?:the\s+|a\s+|an\s+)?(.+)$/i);
|
|
2086
|
+
const target = unwrapTarget((match?.[1] ?? '').trim());
|
|
2087
|
+
if (target.length === 0)
|
|
2088
|
+
return undefined;
|
|
2089
|
+
const normalized = target.replace(/\s+(?:in|from|inside)\s+(?:this\s+)?(?:repo|repository|codebase)$/i, '').trim();
|
|
2090
|
+
if (isGenericReferenceTarget(normalized))
|
|
2091
|
+
return undefined;
|
|
2092
|
+
return normalized;
|
|
2093
|
+
}
|
|
2094
|
+
function isGenericReferenceTarget(target) {
|
|
2095
|
+
return /^(?:it|this|that|thing|symbol|function|method|file|change|changes|break|breaks|breaking|safely|safe|carefully)$/i.test(target);
|
|
2096
|
+
}
|
|
2097
|
+
function extractFileTarget(intent) {
|
|
2098
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2099
|
+
const wrapped = compactIntent.match(/[`'"]([^`'"]+\.[A-Za-z0-9]{1,12})[`'"]/);
|
|
2100
|
+
if (wrapped?.[1] && isFilePathTarget(wrapped[1]))
|
|
2101
|
+
return wrapped[1];
|
|
2102
|
+
const pathMatch = compactIntent.match(/(?:^|\s)([A-Za-z0-9_./:@-]+\.[A-Za-z0-9]{1,12})(?:\s|$)/);
|
|
2103
|
+
if (pathMatch?.[1] && isFilePathTarget(pathMatch[1]))
|
|
2104
|
+
return unwrapTarget(pathMatch[1]);
|
|
2105
|
+
const slashPathMatch = compactIntent.match(/(?:^|\s)([A-Za-z0-9_./:@-]+\/[A-Za-z0-9_./:@-]+)(?:\s|$)/);
|
|
2106
|
+
if (slashPathMatch?.[1] && isFilePathTarget(slashPathMatch[1]))
|
|
2107
|
+
return unwrapTarget(slashPathMatch[1]);
|
|
2108
|
+
return undefined;
|
|
2109
|
+
}
|
|
2110
|
+
function extractEnvVarTarget(intent) {
|
|
2111
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2112
|
+
const processMatch = compactIntent.match(/\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b/);
|
|
2113
|
+
if (processMatch?.[0])
|
|
2114
|
+
return processMatch[0];
|
|
2115
|
+
const envMatch = compactIntent.match(/\b([A-Z][A-Z0-9]*_[A-Z0-9_]+)\b/);
|
|
2116
|
+
return envMatch?.[1];
|
|
2117
|
+
}
|
|
2118
|
+
function extractClaimTarget(intent) {
|
|
2119
|
+
return extractFileTarget(intent) ?? extractSymbolTarget(intent);
|
|
2120
|
+
}
|
|
2121
|
+
function extractClaimAgent(intent) {
|
|
2122
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2123
|
+
const match = compactIntent.match(/\b(?:as|for|agent)\s+([A-Za-z0-9_.:@-]{2,64})\b/i);
|
|
2124
|
+
const candidate = match?.[1];
|
|
2125
|
+
if (!candidate || /^(?:me|myself|us|team|agent|owner)$/i.test(candidate))
|
|
2126
|
+
return undefined;
|
|
2127
|
+
return candidate;
|
|
2128
|
+
}
|
|
2129
|
+
function extractIssueIdTarget(intent) {
|
|
2130
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2131
|
+
const wrapped = compactIntent.match(/[`'"]([^`'"]*[A-Za-z][^`'"]*-[^`'"]+)[`'"]/);
|
|
2132
|
+
if (wrapped?.[1] && isIssueIdTarget(wrapped[1]))
|
|
2133
|
+
return wrapped[1];
|
|
2134
|
+
const labeled = compactIntent.match(/\b(?:issue(?:\s+id)?|id|rule)\s+(?:is\s+|named\s+)?([A-Za-z0-9_:@.-]*[A-Za-z][A-Za-z0-9_:@.-]*-[A-Za-z0-9_:@.-]+)\b/i);
|
|
2135
|
+
if (labeled?.[1] && isIssueIdTarget(labeled[1]))
|
|
2136
|
+
return labeled[1];
|
|
2137
|
+
const issueLike = compactIntent.match(/\b([A-Za-z0-9_:@.-]*[A-Za-z][A-Za-z0-9_:@.-]*-[A-Za-z0-9_:@.-]+)\b/);
|
|
2138
|
+
if (issueLike?.[1] && isIssueIdTarget(issueLike[1]))
|
|
2139
|
+
return issueLike[1];
|
|
2140
|
+
return undefined;
|
|
2141
|
+
}
|
|
2142
|
+
function extractPackageTarget(intent) {
|
|
2143
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2144
|
+
const wrapped = compactIntent.match(/[`'"](@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)[`'"]/);
|
|
2145
|
+
if (wrapped?.[1] && isPackageNameTarget(wrapped[1]))
|
|
2146
|
+
return normalizePackageName(wrapped[1]);
|
|
2147
|
+
const actionMatch = compactIntent.match(/\b(?:bump|upgrade|update|remove|drop|uninstall)\s+(?:the\s+)?(?:(?:package|dependency)\s+)?(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)(?=\s|$)/i);
|
|
2148
|
+
if (actionMatch?.[1] && isPackageNameTarget(actionMatch[1]))
|
|
2149
|
+
return normalizePackageName(actionMatch[1]);
|
|
2150
|
+
const removalSubject = compactIntent.match(/\b(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\s+(?:safe\s+to\s+)?(?:remove|drop|uninstall)\b/i);
|
|
2151
|
+
if (removalSubject?.[1] && isPackageNameTarget(removalSubject[1]))
|
|
2152
|
+
return normalizePackageName(removalSubject[1]);
|
|
2153
|
+
const labeled = compactIntent.match(/\b(?:package|dependency)\s+(?:named\s+|called\s+)?(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)(?=\s|$)/i);
|
|
2154
|
+
if (labeled?.[1] && isPackageNameTarget(labeled[1]))
|
|
2155
|
+
return normalizePackageName(labeled[1]);
|
|
2156
|
+
return undefined;
|
|
2157
|
+
}
|
|
2158
|
+
function extractAuditPackageTarget(intent) {
|
|
2159
|
+
const packageName = extractPackageTarget(intent);
|
|
2160
|
+
if (packageName)
|
|
2161
|
+
return packageName;
|
|
2162
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2163
|
+
const subject = compactIntent.match(/\b(?:does|is|can)\s+(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\s+(?:have|has|contain|contains|affected|vulnerable|secure|safe)\b/i);
|
|
2164
|
+
if (subject?.[1] && isPackageNameTarget(subject[1]))
|
|
2165
|
+
return normalizePackageName(subject[1]);
|
|
2166
|
+
const command = compactIntent.match(/\b(?:audit|check|scan)\s+(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\s+(?:for\s+)?(?:cve|cves|vulnerabilities|vulnerability|security)\b/i);
|
|
2167
|
+
if (command?.[1] && isPackageNameTarget(command[1]))
|
|
2168
|
+
return normalizePackageName(command[1]);
|
|
2169
|
+
return undefined;
|
|
2170
|
+
}
|
|
2171
|
+
function graphQueryFromIntent(intent) {
|
|
2172
|
+
const file = extractFileTarget(intent);
|
|
2173
|
+
const packageName = extractGraphPackageTarget(intent);
|
|
2174
|
+
const symbol = extractSymbolTarget(intent);
|
|
2175
|
+
const direction = graphDirectionFromIntent(intent);
|
|
2176
|
+
if ((direction === 'imports' || direction === 'exports' || direction === 'importers') && file) {
|
|
2177
|
+
return { direction, file };
|
|
2178
|
+
}
|
|
2179
|
+
if (direction === 'package_importers' && packageName) {
|
|
2180
|
+
return { direction, symbol: packageName };
|
|
2181
|
+
}
|
|
2182
|
+
if ((direction === 'symbol_defs' || direction === 'package_importers') && symbol) {
|
|
2183
|
+
return { direction, symbol };
|
|
2184
|
+
}
|
|
2185
|
+
if (direction)
|
|
2186
|
+
return { direction };
|
|
2187
|
+
return undefined;
|
|
2188
|
+
}
|
|
2189
|
+
function graphDirectionFromIntent(intent) {
|
|
2190
|
+
if (/\b(?:who|what|which)\s+uses?\b/.test(intent.toLowerCase()) && !extractFileTarget(intent))
|
|
2191
|
+
return 'package_importers';
|
|
2192
|
+
if (/\b(?:who|what|which)\s+depends?\s+on\b/.test(intent.toLowerCase()) && !extractFileTarget(intent))
|
|
2193
|
+
return 'package_importers';
|
|
2194
|
+
if (/\bwhy\b.*\b(?:depend\s+on|depends\s+on|installed)\b/.test(intent.toLowerCase()) && !extractFileTarget(intent))
|
|
2195
|
+
return 'package_importers';
|
|
2196
|
+
const text = intent.toLowerCase();
|
|
2197
|
+
if (/\b(?:who|what|which)\s+(?:files\s+)?imports?\b/.test(text) && !extractFileTarget(intent))
|
|
2198
|
+
return 'package_importers';
|
|
2199
|
+
if (/\b(?:who|what|which)\s+(?:files\s+)?imports?\b/.test(text) || /\bimporters\b/.test(text))
|
|
2200
|
+
return 'importers';
|
|
2201
|
+
if (/\bexports?\b/.test(text))
|
|
2202
|
+
return 'exports';
|
|
2203
|
+
if (/\bimports?\b/.test(text))
|
|
2204
|
+
return 'imports';
|
|
2205
|
+
if (/\b(?:defined|definition|defines)\b/.test(text))
|
|
2206
|
+
return 'symbol_defs';
|
|
2207
|
+
return undefined;
|
|
2208
|
+
}
|
|
2209
|
+
function graphQueryIsReady(query) {
|
|
2210
|
+
if (query.direction === 'imports' || query.direction === 'exports' || query.direction === 'importers') {
|
|
2211
|
+
return typeof query.file === 'string' && !isPlaceholder(query.file);
|
|
2212
|
+
}
|
|
2213
|
+
if (query.direction === 'symbol_defs' || query.direction === 'package_importers') {
|
|
2214
|
+
return typeof query.symbol === 'string' && !isPlaceholder(query.symbol);
|
|
2215
|
+
}
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
function semanticGraphCommand(query) {
|
|
2219
|
+
const parts = ['projscan semantic-graph', '--query', query.direction];
|
|
2220
|
+
if (query.file)
|
|
2221
|
+
parts.push('--file', isPlaceholder(query.file) ? query.file : quoteShellArg(query.file));
|
|
2222
|
+
if (query.symbol)
|
|
2223
|
+
parts.push('--symbol', isPlaceholder(query.symbol) ? query.symbol : quoteShellArg(query.symbol));
|
|
2224
|
+
if (typeof query.limit === 'number')
|
|
2225
|
+
parts.push('--limit', String(query.limit));
|
|
2226
|
+
parts.push('--format', 'json');
|
|
2227
|
+
return parts.join(' ');
|
|
2228
|
+
}
|
|
2229
|
+
function extractSymbolTarget(intent) {
|
|
2230
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2231
|
+
const wrapped = compactIntent.match(/[`'"]([A-Za-z_$][\w$]*)[`'"]/);
|
|
2232
|
+
if (wrapped?.[1])
|
|
2233
|
+
return wrapped[1];
|
|
2234
|
+
const definitionMatch = compactIntent.match(/\bwhere\s+(?:is|are)\s+(?:the\s+)?([A-Za-z_$][\w$]*)\s+(?:defined|declared|implemented)\b/i);
|
|
2235
|
+
if (definitionMatch?.[1] && isSymbolNameTarget(definitionMatch[1]))
|
|
2236
|
+
return definitionMatch[1];
|
|
2237
|
+
const match = compactIntent.match(/\b(?:symbol|function|class|const|type|interface)\s+([A-Za-z_$][\w$]*)\b/i);
|
|
2238
|
+
return match?.[1] && isSymbolNameTarget(match[1]) ? match[1] : undefined;
|
|
2239
|
+
}
|
|
2240
|
+
function extractGraphPackageTarget(intent) {
|
|
2241
|
+
const compactIntent = intent.trim().replace(/[?!\s]+$/g, '');
|
|
2242
|
+
const importMatch = compactIntent.match(/\b(?:who|what|which)\s+(?:files\s+)?imports?\s+(?:(?:package|dependency)\s+)?(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\b/i);
|
|
2243
|
+
if (importMatch?.[1] && isPackageNameTarget(importMatch[1]))
|
|
2244
|
+
return normalizePackageName(importMatch[1]);
|
|
2245
|
+
const useMatch = compactIntent.match(/\b(?:who|what|which)\s+uses?\s+(?:(?:package|dependency)\s+)?(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\b/i);
|
|
2246
|
+
if (useMatch?.[1] && isPackageNameTarget(useMatch[1]))
|
|
2247
|
+
return normalizePackageName(useMatch[1]);
|
|
2248
|
+
const dependsMatch = compactIntent.match(/\b(?:who|what|which|why(?:\s+do\s+we)?)\s+depends?\s+on\s+(?:(?:package|dependency)\s+)?(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\b/i);
|
|
2249
|
+
if (dependsMatch?.[1] && isPackageNameTarget(dependsMatch[1]))
|
|
2250
|
+
return normalizePackageName(dependsMatch[1]);
|
|
2251
|
+
const installedMatch = compactIntent.match(/\bwhy\s+is\s+(@?[A-Za-z0-9][\w.-]*(?:\/[A-Za-z0-9][\w.-]*)?)\s+installed\b/i);
|
|
2252
|
+
if (installedMatch?.[1] && isPackageNameTarget(installedMatch[1]))
|
|
2253
|
+
return normalizePackageName(installedMatch[1]);
|
|
2254
|
+
return undefined;
|
|
2255
|
+
}
|
|
2256
|
+
function isSymbolNameTarget(target) {
|
|
2257
|
+
return !['symbol', 'function', 'class', 'const', 'type', 'interface', 'defined', 'declared', 'implemented'].includes(target.toLowerCase());
|
|
2258
|
+
}
|
|
2259
|
+
function unwrapTarget(value) {
|
|
2260
|
+
const trimmed = value.trim();
|
|
2261
|
+
const wrapped = trimmed.match(/^([`'"])(.+)\1$/);
|
|
2262
|
+
return (wrapped?.[2] ?? trimmed).trim();
|
|
2263
|
+
}
|
|
2264
|
+
function extractQuotedTextTarget(intent) {
|
|
2265
|
+
const quoted = intent.match(/(["'`])(.{2,200}?)\1/);
|
|
2266
|
+
const target = quoted?.[2]?.trim();
|
|
2267
|
+
return target && !isGenericReferenceTarget(target) ? target : undefined;
|
|
2268
|
+
}
|
|
2269
|
+
function extractObservabilityQuery(intent) {
|
|
2270
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2271
|
+
const logCheck = compactIntent.match(/\b(?:what|which)\s+logs?\s+should\s+i\s+check\s+(?:for|about|on)\s+(.+?)$/i);
|
|
2272
|
+
if (logCheck?.[1])
|
|
2273
|
+
return `${unwrapTarget(logCheck[1].trim())} logs`;
|
|
2274
|
+
const dashboard = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?dashboards?\s+(?:for|about|on)\s+(.+?)$/i);
|
|
2275
|
+
if (dashboard?.[1])
|
|
2276
|
+
return `${unwrapTarget(dashboard[1].trim())} dashboard`;
|
|
2277
|
+
const serviceInit = compactIntent.match(/\b(?:where\s+(?:do|does)\s+(?:we\s+)?(?:initialize|initialise|init)|find|locate|search(?:\s+for)?|lookup)\s+(Sentry|Datadog|Prometheus)\b/i);
|
|
2278
|
+
if (serviceInit?.[1])
|
|
2279
|
+
return serviceInit[1];
|
|
2280
|
+
const observabilityTarget = '(?:metrics?|prometheus\\s+metrics?|alerts?|analytics\\s+events?|events?|sentry\\s+errors?|datadog)';
|
|
2281
|
+
const lookup = compactIntent.match(new RegExp(`\\b(?:where\\s+(?:are|is)|which|what|find|locate|search(?:\\s+for)?|lookup)\\s+(?:the\\s+)?(.*?\\b${observabilityTarget}\\b)(?:\\s+(?:emitted|sent|configured|handled|initialized|initialised|created|defined))?$`, 'i'));
|
|
2282
|
+
if (lookup?.[1] && isObservabilityTarget(lookup[1]))
|
|
2283
|
+
return unwrapTarget(lookup[1].trim()).replace(/^the\s+/i, '');
|
|
2284
|
+
return undefined;
|
|
2285
|
+
}
|
|
2286
|
+
function isObservabilityTarget(target) {
|
|
2287
|
+
return /\b(?:metric|metrics|prometheus|alert|alerts|analytics|events?|sentry|datadog|dashboard|dashboards|logs?)\b/i.test(target);
|
|
2288
|
+
}
|
|
2289
|
+
function extractBackgroundWorkQuery(intent) {
|
|
2290
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2291
|
+
const subjectPattern = 'background\\s+jobs?|cron\\s+jobs?|scheduled\\s+tasks?|queues?\\s+processors?|workers?\\s+processors?|schedulers?|workers?|queues?|processors?';
|
|
2292
|
+
const findMatch = compactIntent.match(new RegExp(`\\b(?:find|locate|search(?:\\s+for)?|lookup)\\s+(?:the\\s+)?(.*?\\b(?:${subjectPattern})\\b)`, 'i'));
|
|
2293
|
+
if (findMatch?.[1] && isBackgroundWorkTarget(findMatch[1]))
|
|
2294
|
+
return unwrapTarget(findMatch[1].trim()).replace(/^the\s+/i, '');
|
|
2295
|
+
const lookupMatch = compactIntent.match(new RegExp(`\\b(?:where\\s+(?:are|is)|which|what)\\s+(?:the\\s+)?(.*?\\b(?:${subjectPattern})\\b)(?:\\s+(?:exist|exists|defined|located|handled|run|runs))?$`, 'i'));
|
|
2296
|
+
if (lookupMatch?.[1] && isBackgroundWorkTarget(lookupMatch[1]))
|
|
2297
|
+
return unwrapTarget(lookupMatch[1].trim()).replace(/^the\s+/i, '');
|
|
2298
|
+
const processMatch = compactIntent.match(/\bwhich\s+(queues?|workers?|processors?)\s+(?:processes?|handles?)\s+(.+?)$/i);
|
|
2299
|
+
if (processMatch?.[1] && processMatch[2])
|
|
2300
|
+
return `${unwrapTarget(processMatch[2].trim())} ${processMatch[1].toLowerCase()}`;
|
|
2301
|
+
return undefined;
|
|
2302
|
+
}
|
|
2303
|
+
function isBackgroundWorkTarget(target) {
|
|
2304
|
+
return /\b(?:background|cron|scheduled|schedule|scheduler|worker|queue|processor)\b/i.test(target);
|
|
2305
|
+
}
|
|
2306
|
+
function extractTestDataQuery(intent) {
|
|
2307
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2308
|
+
if (/\bseeds?\s+data\b|\bdata\s+seeds?\b|\bseed\s+database\b|\bdatabase\s+seed\b/i.test(compactIntent)) {
|
|
2309
|
+
return 'seed data';
|
|
2310
|
+
}
|
|
2311
|
+
const storybook = compactIntent.match(/\b(?:where\s+(?:are|is)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:storybook\s+)?stories\s+(?:for|of)\s+(.+?)$/i);
|
|
2312
|
+
if (storybook?.[1])
|
|
2313
|
+
return `${unwrapTarget(storybook[1].trim())} Storybook stories`;
|
|
2314
|
+
const storyRender = compactIntent.match(/\bwhich\s+stor(?:y|ies)\s+renders?\s+(.+?)$/i);
|
|
2315
|
+
if (storyRender?.[1])
|
|
2316
|
+
return `${unwrapTarget(storyRender[1].trim())} story`;
|
|
2317
|
+
const fixtureLookup = compactIntent.match(/\b(?:where\s+(?:are|is)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:test\s+)?fixtures?\s+(?:for|of)\s+(.+?)$/i);
|
|
2318
|
+
if (fixtureLookup?.[1])
|
|
2319
|
+
return `${unwrapTarget(fixtureLookup[1].trim())} fixtures`;
|
|
2320
|
+
const mockUsage = compactIntent.match(/\bwhich\s+mocks?\s+(?:are\s+)?(?:used|configured)\s+(?:for|by|in)\s+(.+?)$/i);
|
|
2321
|
+
if (mockUsage?.[1])
|
|
2322
|
+
return `${unwrapTarget(mockUsage[1].trim())} mocks`;
|
|
2323
|
+
const factoryLookup = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:are|is))\s+(?:the\s+)?(?:factories?|factory)\s+(?:for|of)\s+(.+?)$/i);
|
|
2324
|
+
if (factoryLookup?.[1])
|
|
2325
|
+
return `${unwrapTarget(factoryLookup[1].trim())} factory`;
|
|
2326
|
+
return undefined;
|
|
2327
|
+
}
|
|
2328
|
+
function extractAuthorizationQuery(intent) {
|
|
2329
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2330
|
+
const rbac = compactIntent.match(/\brbac\b/i);
|
|
2331
|
+
if (rbac)
|
|
2332
|
+
return rbac[0].toUpperCase();
|
|
2333
|
+
const permissionScope = compactIntent.match(/\b(?:where\s+(?:are|is)|which|what|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?permissions?\s+(?:checked\s+)?(?:for|on|in)\s+(.+?)$/i);
|
|
2334
|
+
if (permissionScope?.[1])
|
|
2335
|
+
return `${unwrapTarget(permissionScope[1].trim())} permissions`;
|
|
2336
|
+
const roleAccess = compactIntent.match(/\b(?:which|what)\s+roles?\s+(?:can\s+)?access\s+(.+?)$/i);
|
|
2337
|
+
if (roleAccess?.[1])
|
|
2338
|
+
return `${unwrapTarget(roleAccess[1].trim())} role access`;
|
|
2339
|
+
const guard = compactIntent.match(/\b(?:what|which|where\s+(?:are|is))\s+guards?\s+(?:the\s+)?(.+?)$/i);
|
|
2340
|
+
if (guard?.[1])
|
|
2341
|
+
return `${unwrapTarget(guard[1].trim())} guard`;
|
|
2342
|
+
const policy = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:authorization\s+)?polic(?:y|ies)\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2343
|
+
if (policy?.[1])
|
|
2344
|
+
return `${unwrapTarget(policy[1].trim())} authorization policy`;
|
|
2345
|
+
if (/\b(?:what|which)\s+routes?\s+(?:require|requires|required)\s+login\b/i.test(compactIntent))
|
|
2346
|
+
return 'login routes';
|
|
2347
|
+
if (/\bwhere\s+(?:is|are)\s+login\s+required\b/i.test(compactIntent))
|
|
2348
|
+
return 'login required';
|
|
2349
|
+
return undefined;
|
|
2350
|
+
}
|
|
2351
|
+
function extractReliabilityQuery(intent) {
|
|
2352
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2353
|
+
const scopedRateLimit = compactIntent.match(/\b(?:what|which)\s+rate\s+limits?\s+(?:protects?|guards?|apply\s+to|for)\s+(.+?)$/i);
|
|
2354
|
+
if (scopedRateLimit?.[1])
|
|
2355
|
+
return `${unwrapTarget(scopedRateLimit[1].trim())} rate limits`;
|
|
2356
|
+
if (/\brate\s+limiting\b/i.test(compactIntent))
|
|
2357
|
+
return 'rate limiting';
|
|
2358
|
+
if (/\brate\s+limits?\b/i.test(compactIntent))
|
|
2359
|
+
return 'rate limits';
|
|
2360
|
+
if (/\bthrottl(?:e|ing)\b/i.test(compactIntent))
|
|
2361
|
+
return 'throttling';
|
|
2362
|
+
const cacheFor = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?cache\s+(?:invalidated|cleared|expired|refreshed)\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2363
|
+
if (cacheFor?.[1])
|
|
2364
|
+
return `${unwrapTarget(cacheFor[1].trim())} cache invalidation`;
|
|
2365
|
+
const invalidatesCache = compactIntent.match(/\bwhat\s+invalidates\s+(?:the\s+)?(.+?)\s+cache$/i);
|
|
2366
|
+
if (invalidatesCache?.[1])
|
|
2367
|
+
return `${unwrapTarget(invalidatesCache[1].trim())} cache invalidation`;
|
|
2368
|
+
const cacheConfigured = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?\b(?:cache|redis)\b)\s+(?:configured|defined|created|used|handled)$/i);
|
|
2369
|
+
if (cacheConfigured?.[1])
|
|
2370
|
+
return unwrapTarget(cacheConfigured[1].trim());
|
|
2371
|
+
const retryFor = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?(?:retry|retries|backoff)\s+(?:logic\s+)?(?:for|on|in)\s+(.+?)$/i);
|
|
2372
|
+
if (retryFor?.[1])
|
|
2373
|
+
return `${unwrapTarget(retryFor[1].trim())} retry logic`;
|
|
2374
|
+
const retriesTarget = compactIntent.match(/\b(?:which|what)\s+(?:code\s+)?retries\s+(.+?)$/i);
|
|
2375
|
+
if (retriesTarget?.[1])
|
|
2376
|
+
return `${unwrapTarget(retriesTarget[1].trim())} retries`;
|
|
2377
|
+
if (/\bbackoff\b/i.test(compactIntent))
|
|
2378
|
+
return 'backoff';
|
|
2379
|
+
if (/\bretr(?:y|ies|ied)\b/i.test(compactIntent))
|
|
2380
|
+
return 'retry logic';
|
|
2381
|
+
const timeoutFor = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?timeouts?\s+(?:configured|defined|set)?\s*(?:for|on|in)\s+(.+?)$/i);
|
|
2382
|
+
if (timeoutFor?.[1])
|
|
2383
|
+
return `${unwrapTarget(timeoutFor[1].trim())} timeout`;
|
|
2384
|
+
const timeoutTarget = compactIntent.match(/\b(?:what|which)\s+sets?\s+(.+?\btimeouts?)$/i);
|
|
2385
|
+
if (timeoutTarget?.[1])
|
|
2386
|
+
return unwrapTarget(timeoutTarget[1].trim());
|
|
2387
|
+
if (/\bcircuit\s+breaker\b/i.test(compactIntent))
|
|
2388
|
+
return 'circuit breaker';
|
|
2389
|
+
const idempotency = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?(.+?\bidempotenc(?:y|e)\b.*|idempotent\s+.+?)$/i);
|
|
2390
|
+
if (idempotency?.[1])
|
|
2391
|
+
return unwrapTarget(idempotency[1].trim());
|
|
2392
|
+
if (/\bwebhook\b/i.test(compactIntent) && /\bsignatures?\b/i.test(compactIntent) && /\b(?:verified|verify|verification)\b/i.test(compactIntent)) {
|
|
2393
|
+
return 'webhook signature verification';
|
|
2394
|
+
}
|
|
2395
|
+
if (/\bdebounce(?:d)?\b/i.test(compactIntent))
|
|
2396
|
+
return 'debounce';
|
|
2397
|
+
return undefined;
|
|
2398
|
+
}
|
|
2399
|
+
function extractDataContractQuery(intent) {
|
|
2400
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2401
|
+
const inputValidation = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?input\s+validation\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2402
|
+
if (inputValidation?.[1])
|
|
2403
|
+
return `${unwrapTarget(inputValidation[1].trim())} input validation`;
|
|
2404
|
+
const schemaValidation = compactIntent.match(/\b(?:which|what|where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:zod\s+)?schemas?\s+(?:validates?|for|of)\s+(.+?)$/i);
|
|
2405
|
+
if (schemaValidation?.[1])
|
|
2406
|
+
return `${unwrapTarget(schemaValidation[1].trim())} validation schema`;
|
|
2407
|
+
const validationTarget = compactIntent.match(/\b(?:what|which)\s+validates?\s+(.+?)$/i);
|
|
2408
|
+
if (validationTarget?.[1]) {
|
|
2409
|
+
const target = unwrapTarget(validationTarget[1].trim());
|
|
2410
|
+
if (/\buniqueness\b/i.test(target))
|
|
2411
|
+
return `${target} validation`;
|
|
2412
|
+
return `${target} validation`;
|
|
2413
|
+
}
|
|
2414
|
+
if (/\brequest\s+params?\s+(?:are\s+)?parsed\b/i.test(compactIntent))
|
|
2415
|
+
return 'request params parsing';
|
|
2416
|
+
if (/\bquery\s+params?\b/i.test(compactIntent))
|
|
2417
|
+
return 'query params parsing';
|
|
2418
|
+
const serializesResponse = compactIntent.match(/\b(?:what|which)\s+serializes?\s+(.+?\bresponse)\b/i);
|
|
2419
|
+
if (serializesResponse?.[1])
|
|
2420
|
+
return `${unwrapTarget(serializesResponse[1].trim())} serialization`;
|
|
2421
|
+
const serialization = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?\b(?:serialization|formatting|format)\b)(?:\s+(?:handled|defined|configured))?$/i);
|
|
2422
|
+
if (serialization?.[1])
|
|
2423
|
+
return unwrapTarget(serialization[1].trim());
|
|
2424
|
+
if (/\bdatabase\s+transactions?\b/i.test(compactIntent))
|
|
2425
|
+
return 'database transaction';
|
|
2426
|
+
const transactionTarget = compactIntent.match(/\b(?:what|which)\s+wraps?\s+(.+?)\s+in\s+(?:a\s+)?transactions?\b/i);
|
|
2427
|
+
if (transactionTarget?.[1])
|
|
2428
|
+
return `${unwrapTarget(transactionTarget[1].trim())} transaction`;
|
|
2429
|
+
const rowLock = compactIntent.match(/\b(?:where\s+(?:do|does|is|are)(?:\s+we)?|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:code\s+that\s+)?locks?\s+(?:the\s+)?(.+?\brow)\b/i);
|
|
2430
|
+
if (rowLock?.[1])
|
|
2431
|
+
return `${unwrapTarget(rowLock[1].trim())} lock`;
|
|
2432
|
+
if (/\boptimistic\s+locking\b/i.test(compactIntent))
|
|
2433
|
+
return 'optimistic locking';
|
|
2434
|
+
const uniquenessFor = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?uniqueness\s+(?:enforced|validated|checked)\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2435
|
+
if (uniquenessFor?.[1])
|
|
2436
|
+
return `${unwrapTarget(uniquenessFor[1].trim())} uniqueness`;
|
|
2437
|
+
if (/\bpagination\b/i.test(compactIntent) && /\bcursors?\b/i.test(compactIntent))
|
|
2438
|
+
return 'pagination cursors';
|
|
2439
|
+
if (/\bpagination\b/i.test(compactIntent))
|
|
2440
|
+
return 'pagination';
|
|
2441
|
+
return undefined;
|
|
2442
|
+
}
|
|
2443
|
+
function extractUiInteractionQuery(intent) {
|
|
2444
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2445
|
+
const formSubmitted = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?)\s+forms?\s+(?:submitted|submit|handled)\b/i);
|
|
2446
|
+
if (formSubmitted?.[1])
|
|
2447
|
+
return `${unwrapTarget(formSubmitted[1].trim())} form submit`;
|
|
2448
|
+
const formSubmitFor = compactIntent.match(/\b(?:what|which)\s+handles?\s+forms?\s+submit\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2449
|
+
if (formSubmitFor?.[1])
|
|
2450
|
+
return `${unwrapTarget(formSubmitFor[1].trim())} form submit`;
|
|
2451
|
+
const loadingState = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?loading\s+state\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2452
|
+
if (loadingState?.[1])
|
|
2453
|
+
return `${unwrapTarget(loadingState[1].trim())} loading state`;
|
|
2454
|
+
const emptyState = compactIntent.match(/\b(?:what|which)\s+renders?\s+empty\s+state\s+(?:for|of)\s+(.+?)$/i);
|
|
2455
|
+
if (emptyState?.[1])
|
|
2456
|
+
return `${unwrapTarget(emptyState[1].trim())} empty state`;
|
|
2457
|
+
const errorBoundary = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?error\s+boundary\s+(?:for|on|in)\s+(.+?)$/i);
|
|
2458
|
+
if (errorBoundary?.[1])
|
|
2459
|
+
return `${unwrapTarget(errorBoundary[1].trim())} error boundary`;
|
|
2460
|
+
const toast = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?toast(?:\s+(?:shown|displayed|triggered))?\s+(?:after|for|on|in)\s+(.+?)$/i);
|
|
2461
|
+
if (toast?.[1])
|
|
2462
|
+
return `${unwrapTarget(toast[1].trim())} toast`;
|
|
2463
|
+
const shortcut = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?keyboard\s+shortcuts?\s+(?:for|on)\s+(.+?)$/i);
|
|
2464
|
+
if (shortcut?.[1])
|
|
2465
|
+
return `${unwrapTarget(shortcut[1].trim())} keyboard shortcut`;
|
|
2466
|
+
if (/\bcommand\s+palette\s+actions?\b/i.test(compactIntent))
|
|
2467
|
+
return 'command palette actions';
|
|
2468
|
+
const pageComponent = compactIntent.match(/\b(?:what|which)\s+component\s+renders?\s+(?:the\s+)?(.+?)\s+page$/i);
|
|
2469
|
+
if (pageComponent?.[1])
|
|
2470
|
+
return `${unwrapTarget(pageComponent[1].trim())} page component`;
|
|
2471
|
+
const translations = compactIntent.match(/\b(?:where\s+(?:are|is)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:i18n\s+)?translations?\s+(?:for|of)\s+(.+?)$/i);
|
|
2472
|
+
if (translations?.[1])
|
|
2473
|
+
return `${unwrapTarget(translations[1].trim())} translations`;
|
|
2474
|
+
const aria = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?aria\s+labels?\s+(?:for|on)\s+(.+?)$/i);
|
|
2475
|
+
if (aria?.[1])
|
|
2476
|
+
return `${unwrapTarget(aria[1].trim())} aria label`;
|
|
2477
|
+
if (/\bfocus\s+trap\b/i.test(compactIntent))
|
|
2478
|
+
return 'focus trap';
|
|
2479
|
+
const modal = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?modal\s+(?:opened|shown|displayed)\s+(?:for|on)\s+(.+?)$/i);
|
|
2480
|
+
if (modal?.[1])
|
|
2481
|
+
return `${unwrapTarget(modal[1].trim())} modal`;
|
|
2482
|
+
return undefined;
|
|
2483
|
+
}
|
|
2484
|
+
function extractStyleSystemQuery(intent) {
|
|
2485
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2486
|
+
if (/\b(?:why|failing|failed|failure|failures|broken|error|errors|runtime|production|prod|outage|incident)\b/i.test(compactIntent)) {
|
|
2487
|
+
return undefined;
|
|
2488
|
+
}
|
|
2489
|
+
if (/\bdesign\s+tokens?\b/i.test(compactIntent))
|
|
2490
|
+
return 'design tokens';
|
|
2491
|
+
if (/\btailwind\s+themes?\b/i.test(compactIntent))
|
|
2492
|
+
return 'Tailwind theme';
|
|
2493
|
+
if (/\bglobal\s+css\b/i.test(compactIntent))
|
|
2494
|
+
return 'global CSS';
|
|
2495
|
+
const cssModule = compactIntent.match(/\b(?:which|what)\s+css\s+modules?\s+styles?\s+(.+?)$/i);
|
|
2496
|
+
if (cssModule?.[1])
|
|
2497
|
+
return `${unwrapTarget(cssModule[1].trim())} CSS module`;
|
|
2498
|
+
if (/\bdark\s+mode\b/i.test(compactIntent))
|
|
2499
|
+
return 'dark mode';
|
|
2500
|
+
if (/\bbreakpoints?\b/i.test(compactIntent))
|
|
2501
|
+
return 'breakpoints';
|
|
2502
|
+
return undefined;
|
|
2503
|
+
}
|
|
2504
|
+
function extractNavigationLayoutQuery(intent) {
|
|
2505
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2506
|
+
const sidebarNav = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(?:sidebar\s+)?(?:nav|navigation|menu)\s+items?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2507
|
+
if (sidebarNav?.[1])
|
|
2508
|
+
return `${unwrapTarget(sidebarNav[1].trim())} sidebar nav item`;
|
|
2509
|
+
const breadcrumb = compactIntent.match(/\b(?:which|what)\s+breadcrumbs?\s+(?:renders?|shows?|for|of)\s+(.+?)$/i);
|
|
2510
|
+
if (breadcrumb?.[1])
|
|
2511
|
+
return `${unwrapTarget(breadcrumb[1].trim())} breadcrumb`;
|
|
2512
|
+
const pageTitle = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?page\s+(?:title|metadata|meta)\s+(?:set|sets|defined|configured)\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2513
|
+
if (pageTitle?.[1])
|
|
2514
|
+
return `${unwrapTarget(pageTitle[1].trim())} page title`;
|
|
2515
|
+
const nextLayout = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?next(?:\.js|js)?\s+layouts?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2516
|
+
if (nextLayout?.[1])
|
|
2517
|
+
return `${unwrapTarget(nextLayout[1].trim())} Next.js layout`;
|
|
2518
|
+
return undefined;
|
|
2519
|
+
}
|
|
2520
|
+
function extractFrontendPageRouteQuery(intent) {
|
|
2521
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2522
|
+
if (/\b(?:why|returning|returns|failing|failed|failure|failures|production|prod|down|outage|incident|runtime|crash|crashes|crashing)\b/i.test(compactIntent)) {
|
|
2523
|
+
return undefined;
|
|
2524
|
+
}
|
|
2525
|
+
const pathPage = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(\/[A-Za-z0-9_./:{}-]+)\s+pages?\s+(?:rendered|handled|defined|located|live|lives)\b/i);
|
|
2526
|
+
if (pathPage?.[1])
|
|
2527
|
+
return `${pathPage[1].trim()} page`;
|
|
2528
|
+
const pageRendersPath = compactIntent.match(/\b(?:which|what)\s+pages?\s+(?:renders?|shows?)\s+(\/[A-Za-z0-9_./:{}-]+)\b/i);
|
|
2529
|
+
if (pageRendersPath?.[1])
|
|
2530
|
+
return `${pageRendersPath[1].trim()} page`;
|
|
2531
|
+
const routeSegment = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?routes?\s+segments?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2532
|
+
if (routeSegment?.[1])
|
|
2533
|
+
return `${unwrapTarget(routeSegment[1].trim())} route segment`;
|
|
2534
|
+
if (/\bnot[-\s]?found\s+pages?\s+(?:handled|defined|located|live|lives)\b/i.test(compactIntent))
|
|
2535
|
+
return 'not-found page';
|
|
2536
|
+
if (/\b404\s+pages?\s+(?:handled|defined|located|live|lives)\b/i.test(compactIntent))
|
|
2537
|
+
return '404 page';
|
|
2538
|
+
return undefined;
|
|
2539
|
+
}
|
|
2540
|
+
function extractStateManagementQuery(intent) {
|
|
2541
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2542
|
+
if (/\b(?:pii|gdpr|secret|secrets|token|tokens|password|customer|personal|leak|leaks|leaking|security|retention)\b/i.test(compactIntent))
|
|
2543
|
+
return undefined;
|
|
2544
|
+
const stateStored = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?)\s+state\s+(?:stored|store|stores)\b/i);
|
|
2545
|
+
if (stateStored?.[1])
|
|
2546
|
+
return `${unwrapTarget(stateStored[1].trim())} state store`;
|
|
2547
|
+
const reduxSlice = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?redux\s+slices?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2548
|
+
if (reduxSlice?.[1])
|
|
2549
|
+
return `${unwrapTarget(reduxSlice[1].trim())} Redux slice`;
|
|
2550
|
+
const frameworkStore = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(redux|zustand|jotai|recoil)\s+stores?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2551
|
+
if (frameworkStore?.[1] && frameworkStore[2]) {
|
|
2552
|
+
return `${unwrapTarget(frameworkStore[2].trim())} ${normalizeStateFramework(frameworkStore[1])} store`;
|
|
2553
|
+
}
|
|
2554
|
+
const contextProvider = compactIntent.match(/\b(?:which|what|where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:context\s+)?providers?\s+(?:supplies|supplied|provides|provided|for|of)\s+(.+?)$/i);
|
|
2555
|
+
if (contextProvider?.[1])
|
|
2556
|
+
return `${unwrapTarget(contextProvider[1].trim())} context provider`;
|
|
2557
|
+
const hookFetches = compactIntent.match(/\b(?:which|what)\s+hooks?\s+(?:fetch|fetches|loads?|queries?)\s+(.+?)$/i);
|
|
2558
|
+
if (hookFetches?.[1])
|
|
2559
|
+
return `${unwrapTarget(hookFetches[1].trim())} hook`;
|
|
2560
|
+
const reactQueryMutation = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?react\s+query\s+mutations?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2561
|
+
if (reactQueryMutation?.[1])
|
|
2562
|
+
return `${unwrapTarget(reactQueryMutation[1].trim())} React Query mutation`;
|
|
2563
|
+
const reactQueryQuery = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?react\s+query\s+quer(?:y|ies)\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2564
|
+
if (reactQueryQuery?.[1])
|
|
2565
|
+
return `${unwrapTarget(reactQueryQuery[1].trim())} React Query query`;
|
|
2566
|
+
return undefined;
|
|
2567
|
+
}
|
|
2568
|
+
function extractDataAccessQuery(intent) {
|
|
2569
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2570
|
+
if (/\b(?:sink|sinks|source|taint|injection|xss|vulnerability|security|sanitize|sanitized|reach|reaches|drop|delete|remove)\b/i.test(compactIntent))
|
|
2571
|
+
return undefined;
|
|
2572
|
+
const ormModel = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(prisma|drizzle|typeorm|sequelize)\s+(models?|schemas?|entities?)\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2573
|
+
if (ormModel?.[1] && ormModel[2] && ormModel[3]) {
|
|
2574
|
+
return `${unwrapTarget(ormModel[3].trim())} ${normalizeDataAccessFramework(ormModel[1])} ${normalizeDataAccessArtifact(ormModel[2])}`;
|
|
2575
|
+
}
|
|
2576
|
+
const sqlQuery = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?sql\s+quer(?:y|ies)\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2577
|
+
if (sqlQuery?.[1])
|
|
2578
|
+
return `${unwrapTarget(sqlQuery[1].trim())} SQL query`;
|
|
2579
|
+
const repositorySaves = compactIntent.match(/\b(?:which|what)\s+(?:repository|repositories|dao|daos)\s+(?:saves?|persists?)\s+(.+?)$/i);
|
|
2580
|
+
if (repositorySaves?.[1])
|
|
2581
|
+
return `${unwrapTarget(repositorySaves[1].trim())} repository`;
|
|
2582
|
+
const dataAccessTarget = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?(repository|repositories|dao|daos)\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2583
|
+
if (dataAccessTarget?.[1] && dataAccessTarget[2]) {
|
|
2584
|
+
const artifact = /^dao/i.test(dataAccessTarget[1]) ? 'DAO' : 'repository';
|
|
2585
|
+
return `${unwrapTarget(dataAccessTarget[2].trim())} ${artifact}`;
|
|
2586
|
+
}
|
|
2587
|
+
return undefined;
|
|
2588
|
+
}
|
|
2589
|
+
function extractIntegrationQuery(intent) {
|
|
2590
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2591
|
+
const serviceCall = compactIntent.match(/\bwhere\s+(?:do|does)\s+(?:we\s+)?calls?\s+(.+?)$/i);
|
|
2592
|
+
if (serviceCall?.[1]) {
|
|
2593
|
+
const service = canonicalIntegrationTarget(serviceCall[1]);
|
|
2594
|
+
if (service)
|
|
2595
|
+
return `${service} API`;
|
|
2596
|
+
}
|
|
2597
|
+
const emailProvider = compactIntent.match(/\b(?:which|what)\s+(?:code\s+)?sends?\s+email\s+(?:through|via|with|using)\s+(.+?)$/i);
|
|
2598
|
+
if (emailProvider?.[1]) {
|
|
2599
|
+
const service = canonicalIntegrationTarget(emailProvider[1]);
|
|
2600
|
+
if (service)
|
|
2601
|
+
return `${service} email`;
|
|
2602
|
+
}
|
|
2603
|
+
const storageUpload = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?\bs3\b.*?)\s+(?:upload|uploads|uploaded|implemented|handled|configured)\b/i);
|
|
2604
|
+
if (storageUpload?.[1] && /\bs3\b/i.test(storageUpload[1]))
|
|
2605
|
+
return 'S3 upload';
|
|
2606
|
+
const serviceClient = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?(.+?\b(?:api\s+client|client|sdk)\b)$/i);
|
|
2607
|
+
if (serviceClient?.[1] && isIntegrationTarget(serviceClient[1]))
|
|
2608
|
+
return normalizeIntegrationPhrase(serviceClient[1]);
|
|
2609
|
+
const graphQuery = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?graphql\s+quer(?:y|ies)\s+(?:for|of)\s+(.+?)$/i);
|
|
2610
|
+
if (graphQuery?.[1])
|
|
2611
|
+
return `${unwrapTarget(graphQuery[1].trim())} GraphQL query`;
|
|
2612
|
+
if (/\bwebsockets?\s+connections?\b/i.test(compactIntent) || /\bwebsockets?\s+connection\s+opened\b/i.test(compactIntent))
|
|
2613
|
+
return 'websocket connection';
|
|
2614
|
+
return undefined;
|
|
2615
|
+
}
|
|
2616
|
+
function extractApiContractQuery(intent) {
|
|
2617
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2618
|
+
if (/\bopenapi\b/i.test(compactIntent) && /\bspecs?\b/i.test(compactIntent))
|
|
2619
|
+
return 'OpenAPI spec';
|
|
2620
|
+
if (/\bswagger\b/i.test(compactIntent) && /\bdocs?\b/i.test(compactIntent))
|
|
2621
|
+
return 'Swagger docs';
|
|
2622
|
+
const trpcRouter = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?trpc\s+routers?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2623
|
+
if (trpcRouter?.[1])
|
|
2624
|
+
return `${unwrapTarget(trpcRouter[1].trim())} tRPC router`;
|
|
2625
|
+
const graphqlResolver = compactIntent.match(/\b(?:which|what)\s+graphql\s+resolvers?\s+(?:handles?|for|of)\s+(.+?)$/i);
|
|
2626
|
+
if (graphqlResolver?.[1])
|
|
2627
|
+
return `${unwrapTarget(graphqlResolver[1].trim())} GraphQL resolver`;
|
|
2628
|
+
const graphqlSchema = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?graphql\s+schemas?\s*(?:for|of)?\s*(.*?)$/i);
|
|
2629
|
+
if (graphqlSchema) {
|
|
2630
|
+
const target = unwrapTarget((graphqlSchema[1] ?? '').trim());
|
|
2631
|
+
return target ? `${target} GraphQL schema` : 'GraphQL schema';
|
|
2632
|
+
}
|
|
2633
|
+
const protobuf = compactIntent.match(/\b(?:which|what)\s+(?:protobuf|proto)\s+defines?\s+(.+?)$/i);
|
|
2634
|
+
if (protobuf?.[1])
|
|
2635
|
+
return `${unwrapTarget(protobuf[1].trim())} protobuf`;
|
|
2636
|
+
const grpcClient = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?grpc\s+clients?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2637
|
+
if (grpcClient?.[1])
|
|
2638
|
+
return `${unwrapTarget(grpcClient[1].trim())} gRPC client`;
|
|
2639
|
+
return undefined;
|
|
2640
|
+
}
|
|
2641
|
+
function extractInfraArtifactQuery(intent) {
|
|
2642
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2643
|
+
if (/\bdockerfile\b/i.test(compactIntent))
|
|
2644
|
+
return 'Dockerfile';
|
|
2645
|
+
const dockerCompose = compactIntent.match(/\bdocker\s+compose(?:\s+(?:for|of)\s+(.+?))?$/i);
|
|
2646
|
+
if (dockerCompose) {
|
|
2647
|
+
const target = unwrapTarget((dockerCompose[1] ?? '').trim());
|
|
2648
|
+
return target ? `${target} docker compose` : 'docker compose';
|
|
2649
|
+
}
|
|
2650
|
+
if (/\b(?:kubernetes|k8s)\b/i.test(compactIntent) && /\bmanifests?\b/i.test(compactIntent))
|
|
2651
|
+
return 'Kubernetes manifests';
|
|
2652
|
+
const helm = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?helm\s+charts?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2653
|
+
if (helm?.[1])
|
|
2654
|
+
return `${unwrapTarget(helm[1].trim())} Helm chart`;
|
|
2655
|
+
const terraformModule = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?terraform\s+modules?\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2656
|
+
if (terraformModule?.[1])
|
|
2657
|
+
return `${normalizeInfraTarget(terraformModule[1])} Terraform module`;
|
|
2658
|
+
const githubWorkflow = compactIntent.match(/\b(?:which|what|where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+github\s+workflows?\s+(?:deploys?|for|of|on|in)\s+(.+?)$/i);
|
|
2659
|
+
if (githubWorkflow?.[1])
|
|
2660
|
+
return `${unwrapTarget(githubWorkflow[1].trim())} GitHub workflow`;
|
|
2661
|
+
const hostedConfig = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(vercel|netlify|railway|fly)\s+config(?:uration)?$/i);
|
|
2662
|
+
if (hostedConfig?.[1])
|
|
2663
|
+
return `${normalizeInfraTarget(hostedConfig[1])} config`;
|
|
2664
|
+
return undefined;
|
|
2665
|
+
}
|
|
2666
|
+
function extractToolingConfigQuery(intent) {
|
|
2667
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2668
|
+
if (/\b(?:why|failing|failed|failure|failures|broken|error|errors|runtime|production|prod|outage|incident)\b/i.test(compactIntent)) {
|
|
2669
|
+
return undefined;
|
|
2670
|
+
}
|
|
2671
|
+
if (/\btsconfig\b/i.test(compactIntent) && /\b(?:path|paths|alias|aliases)\b/i.test(compactIntent))
|
|
2672
|
+
return 'tsconfig path aliases';
|
|
2673
|
+
if (/\bvitest\b/i.test(compactIntent) && /\bconfig(?:uration)?\b/i.test(compactIntent))
|
|
2674
|
+
return 'Vitest config';
|
|
2675
|
+
if (/\bjest\b/i.test(compactIntent) && /\bconfig(?:uration)?\b/i.test(compactIntent))
|
|
2676
|
+
return 'Jest config';
|
|
2677
|
+
if (/\bbabel\b/i.test(compactIntent) && /\bconfig(?:uration)?\b/i.test(compactIntent))
|
|
2678
|
+
return 'Babel config';
|
|
2679
|
+
if (/\bwebpack\b/i.test(compactIntent) && /\bconfig(?:uration)?\b/i.test(compactIntent))
|
|
2680
|
+
return 'webpack config';
|
|
2681
|
+
if (/\bpackage\s+manager\b/i.test(compactIntent))
|
|
2682
|
+
return 'package manager';
|
|
2683
|
+
if (/\bpnpm\s+workspaces?\b/i.test(compactIntent))
|
|
2684
|
+
return 'pnpm workspace';
|
|
2685
|
+
if (/\byarn\s+workspaces?\b/i.test(compactIntent))
|
|
2686
|
+
return 'yarn workspace';
|
|
2687
|
+
if (/\b(?:npm|pnpm|yarn)\s+lockfiles?\b/i.test(compactIntent)) {
|
|
2688
|
+
const manager = compactIntent.match(/\b(npm|pnpm|yarn)\b/i)?.[1]?.toLowerCase();
|
|
2689
|
+
return manager ? `${manager} lockfile` : 'lockfile';
|
|
2690
|
+
}
|
|
2691
|
+
return undefined;
|
|
2692
|
+
}
|
|
2693
|
+
function extractDomainWorkflowQuery(intent) {
|
|
2694
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2695
|
+
if (/\bpassword\s+reset\b/i.test(compactIntent))
|
|
2696
|
+
return 'password reset';
|
|
2697
|
+
const invite = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?\binvite\s+flow)\b/i);
|
|
2698
|
+
if (invite?.[1])
|
|
2699
|
+
return unwrapTarget(invite[1].trim());
|
|
2700
|
+
if (/\bonboarding\s+flow\b/i.test(compactIntent))
|
|
2701
|
+
return 'onboarding flow';
|
|
2702
|
+
const csvExport = compactIntent.match(/\b(?:find|locate|search(?:\s+for)?|lookup|where\s+(?:is|are))\s+(?:the\s+)?csv\s+exports?\s+(?:for|of)\s+(.+?)$/i);
|
|
2703
|
+
if (csvExport?.[1])
|
|
2704
|
+
return `${unwrapTarget(csvExport[1].trim())} CSV export`;
|
|
2705
|
+
if (/\baudit\s+logs?\s+entries\b/i.test(compactIntent) || /\baudit\s+log\s+entries\b/i.test(compactIntent))
|
|
2706
|
+
return 'audit log entries';
|
|
2707
|
+
const refund = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?refund\s+handling\s+(?:for|of|on|in)\s+(.+?)$/i);
|
|
2708
|
+
if (refund?.[1])
|
|
2709
|
+
return `${unwrapTarget(refund[1].trim())} refund handling`;
|
|
2710
|
+
if (/\bsubscription\s+renewal\b/i.test(compactIntent))
|
|
2711
|
+
return 'subscription renewal';
|
|
2712
|
+
return undefined;
|
|
2713
|
+
}
|
|
2714
|
+
function extractCommunicationArtifactQuery(intent) {
|
|
2715
|
+
const compactIntent = intent.trim().replace(/[?!.\s]+$/g, '');
|
|
2716
|
+
const welcomeEmail = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?\bemail\s+templates?)\b/i);
|
|
2717
|
+
if (welcomeEmail?.[1])
|
|
2718
|
+
return unwrapTarget(welcomeEmail[1].trim());
|
|
2719
|
+
const emailCopy = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?(.+?\bemail\s+copy)\b/i);
|
|
2720
|
+
if (emailCopy?.[1])
|
|
2721
|
+
return unwrapTarget(emailCopy[1].trim());
|
|
2722
|
+
const pushCopy = compactIntent.match(/\b(?:where\s+(?:is|are)|find|locate|search(?:\s+for)?|lookup)\s+(?:the\s+)?push\s+notifications?\s+copy\s+(?:for|of)\s+(.+?)$/i);
|
|
2723
|
+
if (pushCopy?.[1])
|
|
2724
|
+
return `${unwrapTarget(pushCopy[1].trim())} push notification copy`;
|
|
2725
|
+
if (/\bsms\s+verification\s+templates?\b/i.test(compactIntent))
|
|
2726
|
+
return 'SMS verification template';
|
|
2727
|
+
if (/\breceipt\s+email\b/i.test(compactIntent) && /\btemplates?\b/i.test(compactIntent))
|
|
2728
|
+
return 'receipt email template';
|
|
2729
|
+
if (/\binvoice\s+pdf\b/i.test(compactIntent))
|
|
2730
|
+
return 'invoice PDF';
|
|
2731
|
+
return undefined;
|
|
2732
|
+
}
|
|
2733
|
+
function normalizeInfraTarget(value) {
|
|
2734
|
+
return unwrapTarget(value.trim())
|
|
2735
|
+
.replace(/\bs3\b/gi, 'S3')
|
|
2736
|
+
.replace(/\bvercel\b/gi, 'Vercel')
|
|
2737
|
+
.replace(/\bnetlify\b/gi, 'Netlify')
|
|
2738
|
+
.replace(/\brailway\b/gi, 'Railway')
|
|
2739
|
+
.replace(/\bfly\b/gi, 'Fly');
|
|
2740
|
+
}
|
|
2741
|
+
function canonicalIntegrationTarget(value) {
|
|
2742
|
+
const target = unwrapTarget(value.trim()).replace(/^the\s+/i, '');
|
|
2743
|
+
if (!isIntegrationTarget(target))
|
|
2744
|
+
return undefined;
|
|
2745
|
+
const lower = target.toLowerCase();
|
|
2746
|
+
if (lower === 'stripe')
|
|
2747
|
+
return 'Stripe';
|
|
2748
|
+
if (lower === 'sendgrid')
|
|
2749
|
+
return 'SendGrid';
|
|
2750
|
+
if (lower === 's3' || lower === 'aws s3')
|
|
2751
|
+
return 'S3';
|
|
2752
|
+
if (lower === 'github')
|
|
2753
|
+
return 'GitHub';
|
|
2754
|
+
if (lower === 'graphql')
|
|
2755
|
+
return 'GraphQL';
|
|
2756
|
+
return target;
|
|
2757
|
+
}
|
|
2758
|
+
function normalizeIntegrationPhrase(value) {
|
|
2759
|
+
return value
|
|
2760
|
+
.trim()
|
|
2761
|
+
.replace(/\bgithub\b/gi, 'GitHub')
|
|
2762
|
+
.replace(/\bgraphql\b/gi, 'GraphQL')
|
|
2763
|
+
.replace(/\bstripe\b/gi, 'Stripe')
|
|
2764
|
+
.replace(/\bsendgrid\b/gi, 'SendGrid')
|
|
2765
|
+
.replace(/\bs3\b/gi, 'S3');
|
|
2766
|
+
}
|
|
2767
|
+
function normalizeStateFramework(value) {
|
|
2768
|
+
return value
|
|
2769
|
+
.trim()
|
|
2770
|
+
.replace(/\bredux\b/gi, 'Redux')
|
|
2771
|
+
.replace(/\bzustand\b/gi, 'Zustand')
|
|
2772
|
+
.replace(/\bjotai\b/gi, 'Jotai')
|
|
2773
|
+
.replace(/\brecoil\b/gi, 'Recoil');
|
|
2774
|
+
}
|
|
2775
|
+
function normalizeDataAccessFramework(value) {
|
|
2776
|
+
return value
|
|
2777
|
+
.trim()
|
|
2778
|
+
.replace(/\bprisma\b/gi, 'Prisma')
|
|
2779
|
+
.replace(/\bdrizzle\b/gi, 'Drizzle')
|
|
2780
|
+
.replace(/\btypeorm\b/gi, 'TypeORM')
|
|
2781
|
+
.replace(/\bsequelize\b/gi, 'Sequelize');
|
|
2782
|
+
}
|
|
2783
|
+
function normalizeDataAccessArtifact(value) {
|
|
2784
|
+
const lower = value.trim().toLowerCase();
|
|
2785
|
+
if (lower.startsWith('entit'))
|
|
2786
|
+
return 'entity';
|
|
2787
|
+
if (lower.startsWith('schem'))
|
|
2788
|
+
return 'schema';
|
|
2789
|
+
return 'model';
|
|
2790
|
+
}
|
|
2791
|
+
function isIntegrationTarget(value) {
|
|
2792
|
+
return /\b(?:stripe|sendgrid|s3|aws\s+s3|github|graphql|websocket|websockets?|axios|fetch|rest|http|api\s+client|client|sdk)\b/i.test(value);
|
|
2793
|
+
}
|
|
2794
|
+
function isFilePathTarget(target) {
|
|
2795
|
+
return (target.includes('/') ||
|
|
2796
|
+
target.startsWith('.') ||
|
|
2797
|
+
/\.[A-Za-z0-9]{1,12}$/.test(target)) && !/\s/.test(target);
|
|
2798
|
+
}
|
|
2799
|
+
function isExactSymbolTarget(target) {
|
|
2800
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(target);
|
|
2801
|
+
}
|
|
2802
|
+
function isIssueIdTarget(target) {
|
|
2803
|
+
return (/^[A-Za-z0-9_:@.-]*[A-Za-z][A-Za-z0-9_:@.-]*-[A-Za-z0-9_:@.-]+$/.test(target) &&
|
|
2804
|
+
!target.includes('/') &&
|
|
2805
|
+
target.toLowerCase() !== 'fix-suggest');
|
|
2806
|
+
}
|
|
2807
|
+
function isPackageNameTarget(target) {
|
|
2808
|
+
const lower = target.toLowerCase();
|
|
2809
|
+
if ([
|
|
2810
|
+
'package',
|
|
2811
|
+
'dependency',
|
|
2812
|
+
'dependencies',
|
|
2813
|
+
'version',
|
|
2814
|
+
'latest',
|
|
2815
|
+
'upgrade',
|
|
2816
|
+
'bump',
|
|
2817
|
+
'update',
|
|
2818
|
+
'for',
|
|
2819
|
+
'doc',
|
|
2820
|
+
'docs',
|
|
2821
|
+
'document',
|
|
2822
|
+
'documentation',
|
|
2823
|
+
'documented',
|
|
2824
|
+
'readme',
|
|
2825
|
+
'changelog',
|
|
2826
|
+
'example',
|
|
2827
|
+
'examples',
|
|
2828
|
+
'guide',
|
|
2829
|
+
'should',
|
|
2830
|
+
'could',
|
|
2831
|
+
'would',
|
|
2832
|
+
'can',
|
|
2833
|
+
'what',
|
|
2834
|
+
'which',
|
|
2835
|
+
'the',
|
|
2836
|
+
'this',
|
|
2837
|
+
'that',
|
|
2838
|
+
'it',
|
|
2839
|
+
'my',
|
|
2840
|
+
].includes(lower))
|
|
2841
|
+
return false;
|
|
2842
|
+
if (target.length === 0 || target.length > 214 || target !== target.trim())
|
|
2843
|
+
return false;
|
|
2844
|
+
if (target.includes('..') || target.includes('\\'))
|
|
2845
|
+
return false;
|
|
2846
|
+
return /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(target);
|
|
2847
|
+
}
|
|
2848
|
+
function normalizePackageName(target) {
|
|
2849
|
+
return target.toLowerCase();
|
|
2850
|
+
}
|
|
2851
|
+
function escapeDoubleQuoted(value) {
|
|
2852
|
+
return value
|
|
2853
|
+
.replace(/\\/g, '\\\\')
|
|
2854
|
+
.replace(/"/g, '\\"')
|
|
2855
|
+
.replace(/\$/g, '\\$')
|
|
2856
|
+
.replace(/`/g, '\\`');
|
|
2857
|
+
}
|
|
2858
|
+
function quoteShellArg(value) {
|
|
2859
|
+
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `"${escapeDoubleQuoted(value)}"`;
|
|
2860
|
+
}
|
|
2861
|
+
function quoteShellArgOrPlaceholder(value) {
|
|
2862
|
+
if (isPlaceholder(value))
|
|
2863
|
+
return value;
|
|
2864
|
+
return quoteShellArg(value);
|
|
2865
|
+
}
|
|
2866
|
+
function actionFromFixFirst(fixFirst) {
|
|
2867
|
+
if (!fixFirst)
|
|
2868
|
+
return undefined;
|
|
2869
|
+
return {
|
|
2870
|
+
label: fixFirst.title,
|
|
2871
|
+
command: fixFirst.commands[0],
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function actionFromWorkplan(workplan) {
|
|
2875
|
+
const task = workplan.tasks[0];
|
|
2876
|
+
if (!task)
|
|
2877
|
+
return undefined;
|
|
2878
|
+
return {
|
|
2879
|
+
label: task.title,
|
|
2880
|
+
command: task.verification.commands[0],
|
|
2881
|
+
tool: task.suggestedTools.find((tool) => tool.startsWith('projscan_')),
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
function actionFromWorkflow(workflow) {
|
|
2885
|
+
return {
|
|
2886
|
+
label: `Run ${workflow.name}`,
|
|
2887
|
+
command: workflow.commands[0] ?? 'projscan start --format json',
|
|
2888
|
+
tool: workflow.mcpTools[0],
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
function headlineForStatus(status, label) {
|
|
2892
|
+
if (status === 'blocked')
|
|
2893
|
+
return `Blocked: ${label}`;
|
|
2894
|
+
if (status === 'needs_setup')
|
|
2895
|
+
return `Set up first: ${label}`;
|
|
2896
|
+
if (status === 'needs_attention')
|
|
2897
|
+
return `Proceed carefully: ${label}`;
|
|
2898
|
+
return `Next move: ${label}`;
|
|
2899
|
+
}
|
|
2900
|
+
function missionGuardrails(mode, coordinationHints, primaryAction) {
|
|
2901
|
+
const preflightMode = preflightModeForMission(mode);
|
|
2902
|
+
const guardrails = [
|
|
2903
|
+
{
|
|
2904
|
+
label: 'Verify the repo map before handoff',
|
|
2905
|
+
command: 'projscan understand --view verify --format json',
|
|
2906
|
+
tool: 'projscan_understand',
|
|
2907
|
+
},
|
|
2908
|
+
];
|
|
2909
|
+
if (!isPreflightAction(primaryAction)) {
|
|
2910
|
+
guardrails.unshift({
|
|
2911
|
+
label: 'Check the safety gate before editing',
|
|
2912
|
+
command: `projscan preflight --mode ${preflightMode} --format json`,
|
|
2913
|
+
tool: 'projscan_preflight',
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
for (const hint of coordinationHints.slice(0, 2)) {
|
|
2917
|
+
guardrails.push({
|
|
2918
|
+
label: hint.label,
|
|
2919
|
+
command: hint.command,
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
return dedupeActions(guardrails).slice(0, 4);
|
|
2923
|
+
}
|
|
2924
|
+
function missionProofCommands(mode, workplan, guardrails, actionPlan) {
|
|
2925
|
+
const primaryAction = actionPlan[0] ?? actionFromWorkplan(workplan);
|
|
2926
|
+
const commands = uniqueStrings([
|
|
2927
|
+
...actionPlan.map((action) => action.command ?? ''),
|
|
2928
|
+
...(isPreflightAction(primaryAction) ? [] : [`projscan preflight --mode ${preflightModeForMission(mode)} --format json`]),
|
|
2929
|
+
...guardrails.map((action) => action.command).filter((command) => typeof command === 'string'),
|
|
2930
|
+
...workplan.tasks.flatMap((task) => task.verification.commands),
|
|
2931
|
+
]).filter(isRunnableCommand);
|
|
2932
|
+
if (!isPreflightAction(primaryAction))
|
|
2933
|
+
return commands.slice(0, 8);
|
|
2934
|
+
return commands.filter((command, index) => index === 0 || !command.startsWith('projscan preflight ')).slice(0, 8);
|
|
2935
|
+
}
|
|
2936
|
+
function isRunnableCommand(command) {
|
|
2937
|
+
return !/<[^<>]+>/.test(command);
|
|
2938
|
+
}
|
|
2939
|
+
function missionSuccessCriteria(mode, route, actionPlan, workplan) {
|
|
2940
|
+
const primaryAction = actionPlan[0] ?? actionFromWorkplan(workplan);
|
|
2941
|
+
const criteria = [];
|
|
2942
|
+
if (route?.tool === 'projscan_preflight' || (primaryAction && isPreflightAction(primaryAction))) {
|
|
2943
|
+
const preflightMode = route?.tool === 'projscan_preflight' && primaryAction?.args && 'mode' in primaryAction.args
|
|
2944
|
+
? String(primaryAction.args.mode)
|
|
2945
|
+
: preflightModeForMission(mode);
|
|
2946
|
+
criteria.push(`projscan preflight --mode ${preflightMode} returns proceed or only documented manual-review items.`);
|
|
2947
|
+
criteria.push('Every blocker has an owner, linked file, or follow-up command before the developer continues.');
|
|
2948
|
+
}
|
|
2949
|
+
else if (route?.tool === 'projscan_impact') {
|
|
2950
|
+
if (primaryAction?.tool === 'projscan_search') {
|
|
2951
|
+
criteria.push('An exact symbol or file path is selected from search results before impact analysis continues.');
|
|
2952
|
+
}
|
|
2953
|
+
criteria.push('The impact report is reviewed for direct and transitive dependents before editing starts.');
|
|
2954
|
+
criteria.push('Affected call sites, tests, or owners are added to the workplan before code changes begin.');
|
|
2955
|
+
}
|
|
2956
|
+
else if (route?.tool === 'projscan_release_train') {
|
|
2957
|
+
criteria.push('Release train readiness has no blockers before packaging or publishing continues.');
|
|
2958
|
+
criteria.push('Changelog, package, SBOM, and provenance evidence are reviewed before a release handoff.');
|
|
2959
|
+
}
|
|
2960
|
+
else if (route?.tool === 'projscan_bug_hunt') {
|
|
2961
|
+
criteria.push('Bug-hunt findings are triaged by severity with a first fix candidate selected.');
|
|
2962
|
+
criteria.push('The selected fix has a runnable verification command before editing starts.');
|
|
2963
|
+
}
|
|
2964
|
+
else if (route?.tool === 'projscan_understand') {
|
|
2965
|
+
criteria.push(...understandSuccessCriteria(primaryAction, route));
|
|
2966
|
+
}
|
|
2967
|
+
else if (route?.tool === 'projscan_agent_brief') {
|
|
2968
|
+
criteria.push('The agent brief summarizes focus items, repo context, guardrails, and suggested next actions for the next developer.');
|
|
2969
|
+
criteria.push('The handoff includes enough proof commands for the next agent to resume without rerunning broad discovery.');
|
|
2970
|
+
}
|
|
2971
|
+
else if (route?.tool === 'projscan_session') {
|
|
2972
|
+
criteria.push('Remembered touched files and recent session events are reviewed before resuming work.');
|
|
2973
|
+
criteria.push('The current worktree preflight is rerun after session context is reviewed, so stale memory does not override live risk.');
|
|
2974
|
+
}
|
|
2975
|
+
else if (route?.tool === 'projscan_claim') {
|
|
2976
|
+
const hasAddAction = actionPlan.some((action) => action.args && 'action' in action.args && action.args.action === 'add');
|
|
2977
|
+
if (hasAddAction) {
|
|
2978
|
+
criteria.push('Active claims are reviewed before a new file, directory, or symbol claim is added.');
|
|
2979
|
+
criteria.push('The target is claimed with a real agent name, and any returned contention is assigned or resolved before parallel editing continues.');
|
|
2980
|
+
}
|
|
2981
|
+
else {
|
|
2982
|
+
criteria.push('Active claims, owners, leases, and contention warnings are reviewed before parallel work continues.');
|
|
2983
|
+
criteria.push('Any stale or contended claim has a release, owner, or coordination follow-up before editing resumes.');
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
else if (route?.tool === 'projscan_coordinate') {
|
|
2987
|
+
criteria.push('Coordination readiness, collisions, claims, and merge order are reviewed before parallel work continues.');
|
|
2988
|
+
criteria.push('Any conflicted files, contended claims, or merge-order blockers have an owner or follow-up command before editing resumes.');
|
|
2989
|
+
}
|
|
2990
|
+
else if (route?.tool === 'projscan_privacy_check') {
|
|
2991
|
+
criteria.push('Telemetry state, offline mode, scan root, ignored-file handling, .env content policy, plugin execution, local writes, and network-capable endpoints are reviewed.');
|
|
2992
|
+
criteria.push('Any required trust-boundary change is made explicitly before broader analysis or report sharing continues.');
|
|
2993
|
+
}
|
|
2994
|
+
else if (route?.tool === 'projscan_quality_scorecard') {
|
|
2995
|
+
criteria.push('Quality dimensions, top risks, and verification commands are reviewed before choosing the next task.');
|
|
2996
|
+
criteria.push('The developer knows whether health, security, tests, maintainability, or coordination needs attention first.');
|
|
2997
|
+
}
|
|
2998
|
+
else if (route?.tool === 'projscan_review') {
|
|
2999
|
+
criteria.push('The structural PR review reports a verdict and identifies any risk that needs owner follow-up.');
|
|
3000
|
+
criteria.push('Review, preflight, or evidence-pack follow-up is chosen before the branch is handed to reviewers.');
|
|
3001
|
+
}
|
|
3002
|
+
else if (route?.tool === 'projscan_evidence_pack') {
|
|
3003
|
+
criteria.push('The evidence pack produces a paste-ready PR comment with verdict, top risks, owner routing, and next commands.');
|
|
3004
|
+
criteria.push('The reviewer-facing comment is validated before it is shared or used for approval.');
|
|
3005
|
+
}
|
|
3006
|
+
else if (route?.tool === 'projscan_doctor') {
|
|
3007
|
+
criteria.push('Dead code, unused exports, lint, dependency, security, and config issues are reviewed before cleanup starts.');
|
|
3008
|
+
criteria.push('Any issue chosen for cleanup has a fix-suggest, impact, or verification follow-up before files are deleted.');
|
|
3009
|
+
}
|
|
3010
|
+
else if (route?.tool === 'projscan_fix_suggest') {
|
|
3011
|
+
criteria.push('A concrete fix suggestion is produced for the selected issue id.');
|
|
3012
|
+
criteria.push('The suggestion names the file, fix instruction, and verification step before editing starts.');
|
|
3013
|
+
}
|
|
3014
|
+
else if (route?.tool === 'projscan_explain_issue') {
|
|
3015
|
+
criteria.push('A deep issue explanation is produced for the selected issue id.');
|
|
3016
|
+
criteria.push('The explanation identifies surrounding code, related issues, similar fixes, and the next fix prompt before editing starts.');
|
|
3017
|
+
}
|
|
3018
|
+
else if (route?.tool === 'projscan_regression_plan') {
|
|
3019
|
+
const level = regressionLevelFromPrimaryAction(primaryAction);
|
|
3020
|
+
criteria.push(regressionPlanCriterion(level, route));
|
|
3021
|
+
criteria.push('projscan ci --changed-only or the matching test command is rerun after the selected fix.');
|
|
3022
|
+
}
|
|
3023
|
+
else if (route?.tool === 'projscan_pr_diff') {
|
|
3024
|
+
criteria.push('The structural diff is reviewed for changed exports, imports, call sites, and complexity before a full review verdict.');
|
|
3025
|
+
criteria.push('The developer knows which changed files or symbols need deeper review.');
|
|
3026
|
+
}
|
|
3027
|
+
else if (route?.tool === 'projscan_file') {
|
|
3028
|
+
criteria.push(...fileSuccessCriteria(route));
|
|
3029
|
+
}
|
|
3030
|
+
else if (route?.tool === 'projscan_coverage') {
|
|
3031
|
+
criteria.push('Coverage gaps are ranked by risk so the next test target is explicit.');
|
|
3032
|
+
criteria.push('The selected file has either a new test plan, an owner, or a documented reason to defer.');
|
|
3033
|
+
}
|
|
3034
|
+
else if (route?.tool === 'projscan_upgrade') {
|
|
3035
|
+
criteria.push('The upgrade preview identifies declared version, installed version, breaking markers, and importers.');
|
|
3036
|
+
criteria.push('Importer files are reviewed before changing the package version.');
|
|
3037
|
+
}
|
|
3038
|
+
else if (route?.tool === 'projscan_audit') {
|
|
3039
|
+
criteria.push('npm audit findings are reviewed for critical, high, moderate, low, and info vulnerabilities.');
|
|
3040
|
+
criteria.push('Any vulnerable dependency has a fix, upgrade preview, or documented deferral before the branch is trusted.');
|
|
3041
|
+
}
|
|
3042
|
+
else if (route?.tool === 'projscan_workspaces') {
|
|
3043
|
+
criteria.push('Monorepo workspace packages are listed with names and relative paths before package-scoped work begins.');
|
|
3044
|
+
criteria.push('The selected workspace name is available for package-scoped follow-up commands such as hotspots, coupling, review, or audit.');
|
|
3045
|
+
}
|
|
3046
|
+
else if (route?.tool === 'projscan_dependencies') {
|
|
3047
|
+
if (route.matchedKeywords.some((keyword) => ['license', 'licenses', 'gpl', 'copyleft', 'notice', 'notices', 'third', 'party', 'open', 'source', 'compliance'].includes(keyword))) {
|
|
3048
|
+
criteria.push('Dependency license counts, unknown licenses, and copyleft risks are reviewed before third-party notices or compliance sign-off.');
|
|
3049
|
+
}
|
|
3050
|
+
if (route.matchedKeywords.some((keyword) => ['bundle', 'bundles', 'size', 'sizes', 'large', 'heavy', 'bloat', 'bloated', 'weight', 'footprint', 'reduce', 'slim'].includes(keyword))) {
|
|
3051
|
+
criteria.push('Installed package-size totals and largest packages are reviewed before bundle-size or dependency-bloat work starts.');
|
|
3052
|
+
}
|
|
3053
|
+
criteria.push('Declared production and development dependencies are inventoried before package changes are planned.');
|
|
3054
|
+
criteria.push('Any dependency risks, workspace-specific counts, or missing lockfile signal has an owner or follow-up command.');
|
|
3055
|
+
}
|
|
3056
|
+
else if (route?.tool === 'projscan_dataflow') {
|
|
3057
|
+
criteria.push('Dataflow findings are reviewed for direct, propagated, and bridge source-to-sink paths.');
|
|
3058
|
+
criteria.push('Any confirmed source-to-sink path has an owner, mitigation, and rerunnable verification command before editing continues.');
|
|
3059
|
+
}
|
|
3060
|
+
else if (route?.tool === 'projscan_semantic_graph') {
|
|
3061
|
+
criteria.push('The targeted graph query answers the importer/import/export question without dumping the full graph.');
|
|
3062
|
+
criteria.push('Any returned files are reviewed before editing the queried file or symbol.');
|
|
3063
|
+
}
|
|
3064
|
+
else if (route?.tool === 'projscan_coupling') {
|
|
3065
|
+
const direction = primaryAction?.args && 'direction' in primaryAction.args ? String(primaryAction.args.direction) : 'all';
|
|
3066
|
+
if (direction === 'cycles_only') {
|
|
3067
|
+
criteria.push('Circular-import cycles are reviewed with the exact files participating in each strongly connected component.');
|
|
3068
|
+
}
|
|
3069
|
+
else {
|
|
3070
|
+
criteria.push('Fan-in, fan-out, instability, cross-package edges, and circular-import cycles are reviewed before refactoring boundaries.');
|
|
3071
|
+
}
|
|
3072
|
+
criteria.push('Every high-coupling or circular-import target has an owner, refactor decision, or verification follow-up before architecture work starts.');
|
|
3073
|
+
}
|
|
3074
|
+
else if (route?.tool === 'projscan_search') {
|
|
3075
|
+
criteria.push('Search results identify the target files or symbols with enough confidence to choose the next tool.');
|
|
3076
|
+
}
|
|
3077
|
+
const firstTaskCommand = workplan.tasks[0]?.verification.commands[0];
|
|
3078
|
+
if (firstTaskCommand)
|
|
3079
|
+
criteria.push(`The next task has a verification command: ${firstTaskCommand}`);
|
|
3080
|
+
if (criteria.length === 0) {
|
|
3081
|
+
criteria.push('The primary action returns useful JSON and identifies the next concrete developer step.');
|
|
3082
|
+
criteria.push('At least one proof command is available before handing work to the next agent or human.');
|
|
3083
|
+
}
|
|
3084
|
+
return uniqueStrings(criteria).slice(0, 4);
|
|
3085
|
+
}
|
|
3086
|
+
function fileSuccessCriteria(route) {
|
|
3087
|
+
const matched = new Set(route.matchedKeywords);
|
|
3088
|
+
const criteria = [];
|
|
3089
|
+
if (['risk', 'risks', 'risky', 'dangerous'].some((keyword) => matched.has(keyword))) {
|
|
3090
|
+
criteria.push('Hotspot reasons, related issues, imports, exports, and ownership explain why the file is risky.');
|
|
3091
|
+
}
|
|
3092
|
+
if (['last', 'touched', 'touch', 'changed', 'recently', 'history', 'author', 'authors', 'blame'].some((keyword) => matched.has(keyword))) {
|
|
3093
|
+
criteria.push('Primary author, recent history, and ownership signals are reviewed before routing reviewers or changing the file.');
|
|
3094
|
+
}
|
|
3095
|
+
if (['coverage', 'covered', 'uncovered', 'test', 'tests'].some((keyword) => matched.has(keyword))) {
|
|
3096
|
+
criteria.push('Coverage, hotspot risk, and related test evidence for the file are reviewed before editing starts.');
|
|
3097
|
+
}
|
|
3098
|
+
if (['add', 'write'].some((keyword) => matched.has(keyword)) || (matched.has('test') && !matched.has('coverage') && !matched.has('covered') && !matched.has('uncovered'))) {
|
|
3099
|
+
criteria.push('File purpose, risky functions, coverage, and existing test evidence are reviewed before designing a new test.');
|
|
3100
|
+
}
|
|
3101
|
+
if (matched.has('read')) {
|
|
3102
|
+
criteria.push('Purpose, imports, exports, ownership, tests, and risk are reviewed before changing the named file.');
|
|
3103
|
+
}
|
|
3104
|
+
if (['review', 'reviewer', 'reviewers'].some((keyword) => matched.has(keyword))) {
|
|
3105
|
+
criteria.push('Ownership, primary author, hotspot risk, and related issues are reviewed before choosing a reviewer.');
|
|
3106
|
+
}
|
|
3107
|
+
criteria.push('The file purpose, imports, exports, ownership, and risk are reviewed before editing starts.');
|
|
3108
|
+
criteria.push('Any follow-up impact, owner, or test command from the file report is added to the workplan.');
|
|
3109
|
+
return criteria;
|
|
3110
|
+
}
|
|
3111
|
+
function understandSuccessCriteria(primaryAction, route) {
|
|
3112
|
+
const view = primaryAction?.args && 'view' in primaryAction.args ? String(primaryAction.args.view) : 'map';
|
|
3113
|
+
if (view === 'contracts') {
|
|
3114
|
+
const matched = new Set(route?.matchedKeywords ?? []);
|
|
3115
|
+
if (contractLocalServiceSetupCriteriaMatches(matched)) {
|
|
3116
|
+
return [
|
|
3117
|
+
'Local service startup scripts, container commands, and required config are reviewed before running dev services.',
|
|
3118
|
+
'The developer knows the safest command to start local services plus any env, port, or dependency preconditions.',
|
|
3119
|
+
];
|
|
3120
|
+
}
|
|
3121
|
+
if (contractScriptDiscoveryCriteriaMatches(matched)) {
|
|
3122
|
+
return [
|
|
3123
|
+
'Package scripts, test commands, and config contracts are reviewed before running local commands.',
|
|
3124
|
+
'The developer knows the package-manager command for the requested script plus any required env or setup preconditions.',
|
|
3125
|
+
];
|
|
3126
|
+
}
|
|
3127
|
+
if (contractDatabaseSetupCriteriaMatches(matched)) {
|
|
3128
|
+
return [
|
|
3129
|
+
'Package scripts and config contracts identify the seed, reset, or migration command before shell commands are guessed.',
|
|
3130
|
+
'The developer knows database setup preconditions, required env vars, and the safest local command to run.',
|
|
3131
|
+
];
|
|
3132
|
+
}
|
|
3133
|
+
if (contractEnvCriteriaMatches(matched)) {
|
|
3134
|
+
return [
|
|
3135
|
+
'Required environment variables and config contracts are identified before setup or runtime troubleshooting continues.',
|
|
3136
|
+
'The developer knows which env names, defaults, or config files need local values before running the app.',
|
|
3137
|
+
];
|
|
3138
|
+
}
|
|
3139
|
+
return [
|
|
3140
|
+
'Public exports, config contracts, and likely breaking-change risks are reviewed before touching API surfaces.',
|
|
3141
|
+
'The developer knows which exported files or symbols need compatibility checks.',
|
|
3142
|
+
];
|
|
3143
|
+
}
|
|
3144
|
+
if (view === 'flow') {
|
|
3145
|
+
return [
|
|
3146
|
+
'Runtime entrypoints, flow paths, and side-effect evidence are reviewed before changing request or execution paths.',
|
|
3147
|
+
'The developer knows which files sit on the relevant runtime path.',
|
|
3148
|
+
];
|
|
3149
|
+
}
|
|
3150
|
+
if (view === 'verify') {
|
|
3151
|
+
return [
|
|
3152
|
+
'Verification tiers, direct-test gaps, and likely proof commands are reviewed before pushing or asking for review.',
|
|
3153
|
+
'The developer has the smallest rerunnable command plus the fallback full gate for the intended change.',
|
|
3154
|
+
];
|
|
3155
|
+
}
|
|
3156
|
+
if (view === 'change') {
|
|
3157
|
+
return [
|
|
3158
|
+
'Change-readiness risks, blast radius, and verification tiers are reviewed before editing starts.',
|
|
3159
|
+
'The developer knows which follow-up impact, test, or preflight command gates the change.',
|
|
3160
|
+
];
|
|
3161
|
+
}
|
|
3162
|
+
return [
|
|
3163
|
+
'Read-first files, entrypoints, boundaries, risks, and unknowns are reviewed before editing starts.',
|
|
3164
|
+
'The developer has a cited repo map and knows which files to inspect next.',
|
|
3165
|
+
];
|
|
3166
|
+
}
|
|
3167
|
+
function contractScriptDiscoveryCriteriaMatches(matched) {
|
|
3168
|
+
return ['npm', 'script', 'scripts', 'e2e', 'unit', 'integration', 'storybook', 'cypress', 'playwright', 'eslint', 'prettier', 'format', 'lint', 'typecheck', 'typechecking'].some((keyword) => matched.has(keyword));
|
|
3169
|
+
}
|
|
3170
|
+
function contractLocalServiceSetupCriteriaMatches(matched) {
|
|
3171
|
+
const action = ['run', 'runs', 'start', 'command', 'commands', 'setup'].some((keyword) => matched.has(keyword));
|
|
3172
|
+
const localServices = ['local', 'locally', 'dev'].some((keyword) => matched.has(keyword)) && ['service', 'services', 'server', 'app'].some((keyword) => matched.has(keyword));
|
|
3173
|
+
const dockerCompose = matched.has('docker') && matched.has('compose');
|
|
3174
|
+
return action && (localServices || dockerCompose);
|
|
3175
|
+
}
|
|
3176
|
+
function contractDatabaseSetupCriteriaMatches(matched) {
|
|
3177
|
+
return (['database', 'db', 'migration', 'migrations'].some((keyword) => matched.has(keyword)) &&
|
|
3178
|
+
['seed', 'seeds', 'reset', 'resets', 'migrate', 'migrates', 'run', 'runs', 'command'].some((keyword) => matched.has(keyword)));
|
|
3179
|
+
}
|
|
3180
|
+
function contractEnvCriteriaMatches(matched) {
|
|
3181
|
+
return ['env', 'environment', 'environments', 'vars', 'variable', 'variables', 'missing', 'required'].some((keyword) => matched.has(keyword));
|
|
3182
|
+
}
|
|
3183
|
+
function regressionLevelFromPrimaryAction(primaryAction) {
|
|
3184
|
+
const level = primaryAction?.args && 'level' in primaryAction.args ? String(primaryAction.args.level) : 'focused';
|
|
3185
|
+
if (level === 'smoke' || level === 'focused' || level === 'full')
|
|
3186
|
+
return level;
|
|
3187
|
+
return 'focused';
|
|
3188
|
+
}
|
|
3189
|
+
function regressionPlanCriterion(level, route) {
|
|
3190
|
+
if (level === 'smoke')
|
|
3191
|
+
return 'The smoke regression plan identifies the smallest health and preflight commands to rerun.';
|
|
3192
|
+
if (level === 'full')
|
|
3193
|
+
return 'The full regression plan identifies release-grade build, lint, stability, and test commands to rerun.';
|
|
3194
|
+
if (route && route.matchedKeywords.some((keyword) => ['production', 'prod', 'down', 'outage', 'incident', 'triage', 'runtime', 'crash', 'crashes', 'crashing', '500', '502', '503', '504', '404', '403', '401'].includes(keyword))) {
|
|
3195
|
+
return 'The focused regression plan identifies the smallest high-signal commands to reproduce and verify the failure.';
|
|
3196
|
+
}
|
|
3197
|
+
if (route && route.matchedKeywords.some((keyword) => ['connection', 'refused', 'port', 'ports', 'eaddrinuse', 'listen', 'address', 'permission', 'denied', 'enoent', 'eresolve', 'peer'].includes(keyword))) {
|
|
3198
|
+
return 'The focused regression plan identifies the local setup command, environment symptom, and smallest rerun proof for the blocker.';
|
|
3199
|
+
}
|
|
3200
|
+
return 'The focused regression plan identifies the failing CI or test signal and the smallest verification command to rerun.';
|
|
3201
|
+
}
|
|
3202
|
+
function isPreflightAction(action) {
|
|
3203
|
+
return action.tool === 'projscan_preflight' || action.command?.startsWith('projscan preflight ') === true;
|
|
3204
|
+
}
|
|
3205
|
+
function preflightModeForMission(mode) {
|
|
3206
|
+
if (mode === 'before_commit')
|
|
3207
|
+
return 'before_commit';
|
|
3208
|
+
if (mode === 'hardening')
|
|
3209
|
+
return 'before_commit';
|
|
3210
|
+
if (mode === 'before_merge' || mode === 'release')
|
|
3211
|
+
return 'before_merge';
|
|
3212
|
+
return 'before_edit';
|
|
3213
|
+
}
|
|
180
3214
|
function chooseWorkflow(mode, recipes) {
|
|
181
3215
|
const id = recipeIdForMode(mode);
|
|
182
3216
|
const recipe = recipes.find((entry) => entry.id === id) ?? recipes[0];
|
|
@@ -254,7 +3288,7 @@ function dedupeActions(actions) {
|
|
|
254
3288
|
const seen = new Set();
|
|
255
3289
|
const result = [];
|
|
256
3290
|
for (const action of actions) {
|
|
257
|
-
const key =
|
|
3291
|
+
const key = action.command ? `command:${action.command}` : `action:${action.label}:${action.tool ?? ''}`;
|
|
258
3292
|
if (seen.has(key))
|
|
259
3293
|
continue;
|
|
260
3294
|
seen.add(key);
|
|
@@ -262,6 +3296,9 @@ function dedupeActions(actions) {
|
|
|
262
3296
|
}
|
|
263
3297
|
return result.slice(0, 12);
|
|
264
3298
|
}
|
|
3299
|
+
function uniqueStrings(values) {
|
|
3300
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
3301
|
+
}
|
|
265
3302
|
function summarize(mode, workplan, qualityRisks, adoptionGaps, fixFirstTitle) {
|
|
266
3303
|
return `start: ${mode} recommends ${fixFirstTitle ?? workplan.tasks[0]?.title ?? 'preserving the baseline'} with ${qualityRisks} quality risk(s) and ${adoptionGaps} adoption gap(s)`;
|
|
267
3304
|
}
|