frontend-harness 0.7.9 → 0.8.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/CHANGELOG.md +29 -0
- package/README.md +20 -10
- package/dist/cli/index.js +388 -66
- package/dist/cli/index.js.map +1 -1
- package/dist/runtime/builtin-skills.js +1 -1
- package/dist/runtime/clean.js +25 -3
- package/dist/runtime/clean.js.map +1 -1
- package/dist/runtime/command-taxonomy.js +6 -1
- package/dist/runtime/command-taxonomy.js.map +1 -1
- package/dist/runtime/context.d.ts +33 -2
- package/dist/runtime/context.js +112 -13
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/evidence.d.ts +27 -0
- package/dist/runtime/evidence.js +302 -19
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/graph.js +11 -4
- package/dist/runtime/graph.js.map +1 -1
- package/dist/runtime/ingest.d.ts +38 -0
- package/dist/runtime/ingest.js +157 -0
- package/dist/runtime/ingest.js.map +1 -0
- package/dist/runtime/knowledge/constants.d.ts +9 -0
- package/dist/runtime/knowledge/constants.js +25 -0
- package/dist/runtime/knowledge/constants.js.map +1 -0
- package/dist/runtime/knowledge/core.d.ts +25 -0
- package/dist/runtime/knowledge/core.js +1569 -0
- package/dist/runtime/knowledge/core.js.map +1 -0
- package/dist/runtime/knowledge/types.d.ts +241 -0
- package/dist/runtime/knowledge/types.js +2 -0
- package/dist/runtime/knowledge/types.js.map +1 -0
- package/dist/runtime/knowledge.d.ts +2 -188
- package/dist/runtime/knowledge.js +1 -1126
- package/dist/runtime/knowledge.js.map +1 -1
- package/dist/runtime/plan/component-resolver.js +17 -6
- package/dist/runtime/plan/component-resolver.js.map +1 -1
- package/dist/runtime/plan/guidance.d.ts +1 -1
- package/dist/runtime/plan/guidance.js +88 -25
- package/dist/runtime/plan/guidance.js.map +1 -1
- package/dist/runtime/plan/proposal.js +9 -0
- package/dist/runtime/plan/proposal.js.map +1 -1
- package/dist/runtime/plan/workflow.d.ts +1 -0
- package/dist/runtime/plan/workflow.js +32 -1
- package/dist/runtime/plan/workflow.js.map +1 -1
- package/dist/runtime/plan.d.ts +13 -0
- package/dist/runtime/plan.js +137 -82
- package/dist/runtime/plan.js.map +1 -1
- package/dist/runtime/policy-provenance.d.ts +17 -1
- package/dist/runtime/policy-provenance.js +84 -16
- package/dist/runtime/policy-provenance.js.map +1 -1
- package/dist/runtime/project-discovery.js +32 -17
- package/dist/runtime/project-discovery.js.map +1 -1
- package/dist/runtime/project-paths.js +7 -1
- package/dist/runtime/project-paths.js.map +1 -1
- package/dist/runtime/protocol-init.js +117 -26
- package/dist/runtime/protocol-init.js.map +1 -1
- package/dist/runtime/repair-decision.js +76 -16
- package/dist/runtime/repair-decision.js.map +1 -1
- package/dist/runtime/repair-packet.js +141 -20
- package/dist/runtime/repair-packet.js.map +1 -1
- package/dist/runtime/scaffold/vue-template.js +1 -1
- package/dist/runtime/scaffold/vue-template.js.map +1 -1
- package/dist/runtime/scaffold.js +44 -8
- package/dist/runtime/scaffold.js.map +1 -1
- package/dist/runtime/skills.d.ts +9 -0
- package/dist/runtime/skills.js +133 -18
- package/dist/runtime/skills.js.map +1 -1
- package/dist/runtime/state-explain.js +39 -7
- package/dist/runtime/state-explain.js.map +1 -1
- package/dist/runtime/state.js +85 -10
- package/dist/runtime/state.js.map +1 -1
- package/dist/runtime/task-context.d.ts +122 -0
- package/dist/runtime/task-context.js +1322 -0
- package/dist/runtime/task-context.js.map +1 -0
- package/dist/runtime/task-signals.d.ts +1 -0
- package/dist/runtime/task-signals.js +1 -1
- package/dist/runtime/task-signals.js.map +1 -1
- package/dist/runtime/ui-restoration.d.ts +3 -3
- package/dist/runtime/ui-restoration.js +3 -3
- package/dist/runtime/ui-restoration.js.map +1 -1
- package/dist/runtime/ui-source.d.ts +6 -0
- package/dist/runtime/ui-source.js +66 -0
- package/dist/runtime/ui-source.js.map +1 -0
- package/dist/runtime/units.js +11 -4
- package/dist/runtime/units.js.map +1 -1
- package/dist/runtime/verification-commands.js +95 -21
- package/dist/runtime/verification-commands.js.map +1 -1
- package/dist/runtime/verify.js +129 -43
- package/dist/runtime/verify.js.map +1 -1
- package/dist/runtime/visual-compare.js +104 -12
- package/dist/runtime/visual-compare.js.map +1 -1
- package/dist/schemas/types.d.ts +75 -2
- package/dist/schemas/validation.js +257 -6
- package/dist/schemas/validation.js.map +1 -1
- package/dist/storage/json.d.ts +9 -1
- package/dist/storage/json.js +106 -8
- package/dist/storage/json.js.map +1 -1
- package/docs/DIRECTION.md +13 -11
- package/docs/RELEASE.md +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1322 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { relativeHarnessPath } from "../storage/paths.js";
|
|
4
|
+
import { writeJson } from "../storage/json.js";
|
|
5
|
+
import { isRegularFileWithoutSymlink } from "../storage/json.js";
|
|
6
|
+
import { discoverKnowledgeCards } from "./knowledge.js";
|
|
7
|
+
import { listIngestRecords } from "./ingest.js";
|
|
8
|
+
import { inferFrontendWorkflow, inferWorkflowPlan, verificationForWorkflow } from "./plan/workflow.js";
|
|
9
|
+
import { resolveProjectRelativePath } from "./project-paths.js";
|
|
10
|
+
import { discoverProjectSurface } from "./project-discovery.js";
|
|
11
|
+
import { externalDesignReference, inferLocalUiSourceKind, isGeneratedMarkupExtension, isVisualArtifactExtension, unsupportedDesignSourceUrlMessage } from "./ui-source.js";
|
|
12
|
+
const DEFAULT_CONTEXT_CARD_LIMIT = 8;
|
|
13
|
+
const MAX_CONTEXT_CARD_LIMIT = 20;
|
|
14
|
+
const PROJECT_FILE_LIMIT = 8;
|
|
15
|
+
const MAX_PROJECT_FILES_SCANNED = 300;
|
|
16
|
+
const SOURCE_HINT_LIMIT = 8;
|
|
17
|
+
const INGEST_SOURCE_LIMIT = 8;
|
|
18
|
+
const MAX_API_SOURCE_HINT_BYTES = 256 * 1024;
|
|
19
|
+
const MAX_PRD_SOURCE_HINT_BYTES = 128 * 1024;
|
|
20
|
+
const MAX_PRD_HEADINGS_SCANNED = 40;
|
|
21
|
+
const KNOWLEDGE_CARD_FIELD_VALUE_LIMIT = 8;
|
|
22
|
+
const INGEST_PRODUCED_CARD_ID_LIMIT = 8;
|
|
23
|
+
const MAX_PROMPT_FIELD_CHARS = 240;
|
|
24
|
+
const PROJECT_FILE_EXTENSIONS = new Set([".vue", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".css", ".scss", ".sass", ".less"]);
|
|
25
|
+
const PROJECT_FILE_EXTRA_ROOTS = [
|
|
26
|
+
"api",
|
|
27
|
+
"apis",
|
|
28
|
+
"client",
|
|
29
|
+
"clients",
|
|
30
|
+
"service",
|
|
31
|
+
"services",
|
|
32
|
+
"tests",
|
|
33
|
+
"test",
|
|
34
|
+
"e2e",
|
|
35
|
+
"playwright",
|
|
36
|
+
"cypress"
|
|
37
|
+
];
|
|
38
|
+
const IGNORED_DIRECTORIES = new Set([".frontend-harness", ".git", "build", "coverage", "dist", "node_modules", ".cache", ".turbo"]);
|
|
39
|
+
const SEPARATED_TOKEN_STOP_WORDS = new Set(["center", "docs", "prd", "spec", "md", "json", "yaml", "yml"]);
|
|
40
|
+
export function buildCompactTaskContext(projectRoot, options) {
|
|
41
|
+
const task = options.taskText.trim();
|
|
42
|
+
if (!task) {
|
|
43
|
+
throw new Error("context task requires <task>");
|
|
44
|
+
}
|
|
45
|
+
const limit = normalizeLimit(options.limit);
|
|
46
|
+
const inputs = {
|
|
47
|
+
...(options.prd ? { prd: normalizeTaskPrdReference(projectRoot, options.prd) } : {}),
|
|
48
|
+
...(options.apiSource ? { apiSource: normalizeTaskApiSourceReference(projectRoot, options.apiSource) } : {}),
|
|
49
|
+
...(options.uiSource ? { uiSource: normalizeTaskUiSourceReference(projectRoot, options.uiSource) } : {})
|
|
50
|
+
};
|
|
51
|
+
const workflow = inferFrontendWorkflow(task, inputs, null);
|
|
52
|
+
const workflowPlan = inferWorkflowPlan(workflow, task, inputs, null);
|
|
53
|
+
const selectedKnowledge = selectKnowledge(projectRoot, task, limit, inputs);
|
|
54
|
+
const compactKnowledge = compactKnowledgeCards(selectedKnowledge.cards);
|
|
55
|
+
const selectedIngest = selectIngestSources(projectRoot, task, selectedKnowledge.cards, inputs);
|
|
56
|
+
const compactIngest = compactIngestSources(selectedIngest.records);
|
|
57
|
+
const selectedProjectFiles = selectProjectFiles(projectRoot, task, inputs, selectedKnowledge.cards);
|
|
58
|
+
const sourceHints = buildSourceHints(projectRoot, task, inputs, selectedKnowledge.cards, selectedIngest.records);
|
|
59
|
+
const limits = compactTaskContextLimits(limit, selectedKnowledge.omitted, compactKnowledge.omittedSourcePaths, compactKnowledge.omittedCoverage, compactKnowledge.omittedVerification, selectedIngest.omitted, compactIngest.omittedProducedKnowledgeCardIds, selectedProjectFiles.omitted, sourceHints.omitted, sourceHints.omittedPrdCoverageHintCoverage, sourceHints.omittedPrdCoverageHintAnchors, sourceHints.omittedPrdCoverageHintSourcePaths, selectedProjectFiles.scanned, selectedProjectFiles.scanLimitReached, sourceHints.skippedPrdSourceHintSources, sourceHints.skippedApiSourceHintSources);
|
|
60
|
+
const context = {
|
|
61
|
+
contractVersion: 1,
|
|
62
|
+
task,
|
|
63
|
+
inputs,
|
|
64
|
+
workflow: {
|
|
65
|
+
kind: workflow.kind,
|
|
66
|
+
summary: workflow.summary,
|
|
67
|
+
verificationFocus: verificationForWorkflow(workflow.kind)
|
|
68
|
+
},
|
|
69
|
+
selectedKnowledge: compactKnowledge.cards,
|
|
70
|
+
selectedIngestSources: compactIngest.records,
|
|
71
|
+
selectedProjectFiles: selectedProjectFiles.files,
|
|
72
|
+
sourceHints: sourceHints.hints,
|
|
73
|
+
limits,
|
|
74
|
+
prompt: renderPrompt(task, inputs, workflow.kind, workflow.summary, workflowPlan.stages.map((stage) => stage.kind), selectedKnowledge.cards, selectedIngest.records, selectedProjectFiles.files, sourceHints.hints, limits),
|
|
75
|
+
artifactPath: relativeHarnessPath("context", "task-latest.json")
|
|
76
|
+
};
|
|
77
|
+
writeJson(path.join(projectRoot, context.artifactPath), context);
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
80
|
+
function selectKnowledge(projectRoot, task, limit, inputs) {
|
|
81
|
+
const tokens = taskTokens(task);
|
|
82
|
+
const sourceReferences = taskInputSourceReferences(projectRoot, inputs);
|
|
83
|
+
const matches = discoverKnowledgeCards(projectRoot)
|
|
84
|
+
.filter((card) => card.status === "active")
|
|
85
|
+
.map((card) => ({
|
|
86
|
+
card,
|
|
87
|
+
score: scoreKnowledgeCard(card, tokens, sourceReferences)
|
|
88
|
+
}))
|
|
89
|
+
.filter((item) => item.score > 0)
|
|
90
|
+
.sort((left, right) => right.score - left.score || left.card.path.localeCompare(right.card.path));
|
|
91
|
+
const cards = matches.slice(0, limit).map((item) => item.card);
|
|
92
|
+
return {
|
|
93
|
+
cards,
|
|
94
|
+
omitted: Math.max(0, matches.length - cards.length)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function taskInputSourceReferences(projectRoot, inputs) {
|
|
98
|
+
const references = {
|
|
99
|
+
prd: new Set(inputs.prd ? [normalizeSourcePath(inputs.prd)] : []),
|
|
100
|
+
api: new Set(inputs.apiSource ? [normalizeSourcePath(inputs.apiSource)] : []),
|
|
101
|
+
ui: new Set(inputs.uiSource ? [normalizeSourcePath(inputs.uiSource.value)] : [])
|
|
102
|
+
};
|
|
103
|
+
const records = listIngestRecords(projectRoot, undefined, { limit: "all" }).records;
|
|
104
|
+
addMatchingIngestSourceReferences(references.prd, records, "prd", inputs.prd);
|
|
105
|
+
addMatchingIngestSourceReferences(references.api, records, "openapi", inputs.apiSource);
|
|
106
|
+
addMatchingIngestSourceReferences(references.ui, records, "ui-package", inputs.uiSource?.value);
|
|
107
|
+
return references;
|
|
108
|
+
}
|
|
109
|
+
function addMatchingIngestSourceReferences(references, records, kind, source) {
|
|
110
|
+
if (!source) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const record of records) {
|
|
114
|
+
if (record.kind === kind && sameSourceReference(record.source.value, source)) {
|
|
115
|
+
references.add(normalizeSourcePath(record.ingestRef));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function selectIngestSources(projectRoot, task, cards, inputs) {
|
|
120
|
+
const tokens = taskTokens(task);
|
|
121
|
+
const sourceRefs = new Set(cards.flatMap((card) => card.sourcePaths).map(ingestRefKey).filter((sourcePath) => sourcePath !== null));
|
|
122
|
+
const inputSources = [inputs.prd, inputs.apiSource, inputs.uiSource?.value].filter((source) => Boolean(source));
|
|
123
|
+
const matches = listIngestRecords(projectRoot, undefined, { limit: "all" }).records
|
|
124
|
+
.map((record) => ({
|
|
125
|
+
record,
|
|
126
|
+
score: (sourceRefs.has(ingestRefKey(record.ingestRef) ?? record.ingestRef) ? 100 : 0)
|
|
127
|
+
+ (inputSources.some((source) => sameSourceReference(record.source.value, source)) ? 100 : 0)
|
|
128
|
+
+ scoreText(`${record.ingestRef} ${record.summary} ${record.source.value}`, tokens)
|
|
129
|
+
}))
|
|
130
|
+
.filter((item) => item.score > 0)
|
|
131
|
+
.sort((left, right) => right.score - left.score || left.record.ingestRef.localeCompare(right.record.ingestRef));
|
|
132
|
+
const records = matches.slice(0, INGEST_SOURCE_LIMIT).map((item) => item.record);
|
|
133
|
+
return {
|
|
134
|
+
records,
|
|
135
|
+
omitted: Math.max(0, matches.length - records.length)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function selectProjectFiles(projectRoot, task, inputs, cards) {
|
|
139
|
+
const baseTokens = taskTokens([task, inputs.prd ?? "", inputs.apiSource ?? "", inputs.uiSource?.value ?? ""].join(" "));
|
|
140
|
+
const knowledgeTokens = taskTokens(projectFileKnowledgeSignals(cards));
|
|
141
|
+
const surface = discoverProjectSurface(projectRoot);
|
|
142
|
+
const candidates = scanProjectFiles(projectRoot, candidateProjectFileRoots(projectRoot, surface.sourceRoots));
|
|
143
|
+
const matches = candidates
|
|
144
|
+
.map((file) => {
|
|
145
|
+
const baseScore = scoreText(file, baseTokens);
|
|
146
|
+
const knowledgeScore = scoreText(file, knowledgeTokens);
|
|
147
|
+
return {
|
|
148
|
+
path: file,
|
|
149
|
+
score: baseScore + knowledgeScore,
|
|
150
|
+
reason: projectFileMatchReason(baseScore, knowledgeScore)
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
.filter((item) => item.score > 0)
|
|
154
|
+
.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
155
|
+
const files = matches.slice(0, PROJECT_FILE_LIMIT).map((item) => ({
|
|
156
|
+
path: item.path,
|
|
157
|
+
reason: item.reason
|
|
158
|
+
}));
|
|
159
|
+
return {
|
|
160
|
+
files,
|
|
161
|
+
omitted: Math.max(0, matches.length - files.length),
|
|
162
|
+
scanned: candidates.length,
|
|
163
|
+
scanLimitReached: candidates.length >= MAX_PROJECT_FILES_SCANNED
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function candidateProjectFileRoots(projectRoot, sourceRoots) {
|
|
167
|
+
return minimizeProjectFileRoots([...new Set([
|
|
168
|
+
...sourceRoots,
|
|
169
|
+
...PROJECT_FILE_EXTRA_ROOTS.filter((root) => {
|
|
170
|
+
const stat = fs.lstatSync(path.join(projectRoot, root), { throwIfNoEntry: false });
|
|
171
|
+
return stat?.isDirectory() && !stat.isSymbolicLink();
|
|
172
|
+
})
|
|
173
|
+
])]);
|
|
174
|
+
}
|
|
175
|
+
function minimizeProjectFileRoots(roots) {
|
|
176
|
+
return roots.filter((root) => !roots.some((candidate) => candidate !== root && isProjectRootParent(candidate, root)));
|
|
177
|
+
}
|
|
178
|
+
function isProjectRootParent(parent, child) {
|
|
179
|
+
const normalizedParent = parent.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
180
|
+
const normalizedChild = child.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
181
|
+
return normalizedChild.startsWith(`${normalizedParent}/`);
|
|
182
|
+
}
|
|
183
|
+
function projectFileKnowledgeSignals(cards) {
|
|
184
|
+
return cards.map((card) => [
|
|
185
|
+
card.id,
|
|
186
|
+
card.title,
|
|
187
|
+
card.subject ?? "",
|
|
188
|
+
...card.scope,
|
|
189
|
+
...card.tags,
|
|
190
|
+
...card.sourcePaths
|
|
191
|
+
].join(" ")).join(" ");
|
|
192
|
+
}
|
|
193
|
+
function projectFileMatchReason(baseScore, knowledgeScore) {
|
|
194
|
+
if (baseScore > 0 && knowledgeScore > 0) {
|
|
195
|
+
return "matched task and selected knowledge terms in project file path";
|
|
196
|
+
}
|
|
197
|
+
if (knowledgeScore > 0) {
|
|
198
|
+
return "matched selected knowledge terms in project file path";
|
|
199
|
+
}
|
|
200
|
+
return "matched task terms in project file path";
|
|
201
|
+
}
|
|
202
|
+
function scanProjectFiles(projectRoot, sourceRoots) {
|
|
203
|
+
const files = [];
|
|
204
|
+
for (const root of sourceRoots) {
|
|
205
|
+
collectProjectFiles(projectRoot, root, files, 0);
|
|
206
|
+
if (files.length >= MAX_PROJECT_FILES_SCANNED) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return [...new Set(files)].slice(0, MAX_PROJECT_FILES_SCANNED);
|
|
211
|
+
}
|
|
212
|
+
function collectProjectFiles(projectRoot, relativeDir, files, depth) {
|
|
213
|
+
if (files.length >= MAX_PROJECT_FILES_SCANNED || depth > 5) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const fullDir = path.join(projectRoot, relativeDir);
|
|
217
|
+
const stat = fs.lstatSync(fullDir, { throwIfNoEntry: false });
|
|
218
|
+
if (!stat?.isDirectory() || stat.isSymbolicLink()) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
for (const entry of fs.readdirSync(fullDir, { withFileTypes: true })) {
|
|
222
|
+
if (files.length >= MAX_PROJECT_FILES_SCANNED) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const relativePath = path.posix.join(relativeDir.replace(/\\/g, "/"), entry.name);
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
228
|
+
collectProjectFiles(projectRoot, relativePath, files, depth + 1);
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (entry.isFile() && PROJECT_FILE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
233
|
+
files.push(relativePath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function compactKnowledgeCards(cards) {
|
|
238
|
+
return cards.reduce((result, card) => {
|
|
239
|
+
result.cards.push(compactKnowledgeCard(card));
|
|
240
|
+
result.omittedSourcePaths += omittedFieldValues(card.sourcePaths);
|
|
241
|
+
result.omittedCoverage += omittedFieldValues(card.coverage);
|
|
242
|
+
result.omittedVerification += omittedFieldValues(card.verification);
|
|
243
|
+
return result;
|
|
244
|
+
}, {
|
|
245
|
+
cards: [],
|
|
246
|
+
omittedSourcePaths: 0,
|
|
247
|
+
omittedCoverage: 0,
|
|
248
|
+
omittedVerification: 0
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function compactKnowledgeCard(card) {
|
|
252
|
+
return {
|
|
253
|
+
id: card.id,
|
|
254
|
+
title: card.title,
|
|
255
|
+
summary: card.summary,
|
|
256
|
+
type: card.type,
|
|
257
|
+
kind: card.kind ?? null,
|
|
258
|
+
path: card.path,
|
|
259
|
+
sourcePaths: limitFieldValues(card.sourcePaths),
|
|
260
|
+
coverage: limitFieldValues(card.coverage),
|
|
261
|
+
verification: limitFieldValues(card.verification)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function limitFieldValues(values) {
|
|
265
|
+
return values.slice(0, KNOWLEDGE_CARD_FIELD_VALUE_LIMIT);
|
|
266
|
+
}
|
|
267
|
+
function omittedFieldValues(values) {
|
|
268
|
+
return Math.max(0, values.length - KNOWLEDGE_CARD_FIELD_VALUE_LIMIT);
|
|
269
|
+
}
|
|
270
|
+
function compactIngestSources(records) {
|
|
271
|
+
return records.reduce((result, record) => {
|
|
272
|
+
result.records.push(compactIngestSource(record));
|
|
273
|
+
result.omittedProducedKnowledgeCardIds += Math.max(0, record.producedKnowledgeCardIds.length - INGEST_PRODUCED_CARD_ID_LIMIT);
|
|
274
|
+
return result;
|
|
275
|
+
}, {
|
|
276
|
+
records: [],
|
|
277
|
+
omittedProducedKnowledgeCardIds: 0
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function compactIngestSource(record) {
|
|
281
|
+
return {
|
|
282
|
+
ingestRef: record.ingestRef,
|
|
283
|
+
kind: record.kind,
|
|
284
|
+
summary: record.summary,
|
|
285
|
+
source: record.source.value,
|
|
286
|
+
sourceType: record.source.type,
|
|
287
|
+
sha256: record.source.sha256,
|
|
288
|
+
sizeBytes: record.source.sizeBytes,
|
|
289
|
+
producedKnowledgeCardIds: record.producedKnowledgeCardIds.slice(0, INGEST_PRODUCED_CARD_ID_LIMIT),
|
|
290
|
+
artifactPath: record.artifactPath
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function buildSourceHints(projectRoot, task, inputs, cards, ingestSources) {
|
|
294
|
+
const prdCoverageCandidates = selectPrdCoverageHints(projectRoot, task, inputs, cards, ingestSources);
|
|
295
|
+
const uiArtifactSelection = selectUiArtifactHints(task, inputs, ingestSources, cards);
|
|
296
|
+
const uiArtifactCandidates = uiArtifactSelection.hints;
|
|
297
|
+
const apiImplementationSelection = selectApiImplementationHints(projectRoot, task, inputs, cards, ingestSources);
|
|
298
|
+
const apiImplementationCandidates = apiImplementationSelection.hints;
|
|
299
|
+
const endpointCandidates = selectEndpointHints(projectRoot, task, inputs, cards, ingestSources);
|
|
300
|
+
const endpoints = endpointCandidates.slice(0, sourceHintSliceLimit([], [prdCoverageCandidates, uiArtifactCandidates, apiImplementationCandidates]));
|
|
301
|
+
const prdCoverageSelections = prdCoverageCandidates.slice(0, sourceHintSliceLimit([endpoints], [uiArtifactCandidates, apiImplementationCandidates]));
|
|
302
|
+
const prdCoverage = prdCoverageSelections.map((selection) => selection.hint);
|
|
303
|
+
const uiArtifacts = uiArtifactCandidates.slice(0, sourceHintSliceLimit([endpoints, prdCoverage], [apiImplementationCandidates]));
|
|
304
|
+
const apiImplementations = apiImplementationCandidates.slice(0, remainingSourceHintBudget(endpoints, prdCoverage, uiArtifacts));
|
|
305
|
+
return {
|
|
306
|
+
hints: {
|
|
307
|
+
endpoints,
|
|
308
|
+
prdCoverage,
|
|
309
|
+
uiArtifacts,
|
|
310
|
+
apiImplementations
|
|
311
|
+
},
|
|
312
|
+
omitted: Math.max(0, endpointCandidates.length - endpoints.length)
|
|
313
|
+
+ Math.max(0, prdCoverageCandidates.length - prdCoverage.length)
|
|
314
|
+
+ Math.max(0, uiArtifactCandidates.length - uiArtifacts.length)
|
|
315
|
+
+ uiArtifactSelection.omitted
|
|
316
|
+
+ Math.max(0, apiImplementationCandidates.length - apiImplementations.length)
|
|
317
|
+
+ apiImplementationSelection.omitted,
|
|
318
|
+
omittedPrdCoverageHintCoverage: prdCoverageSelections.reduce((total, selection) => total + selection.omittedCoverage, 0),
|
|
319
|
+
omittedPrdCoverageHintAnchors: prdCoverageSelections.reduce((total, selection) => total + selection.omittedAnchors, 0),
|
|
320
|
+
omittedPrdCoverageHintSourcePaths: prdCoverageSelections.reduce((total, selection) => total + selection.omittedSourcePaths, 0),
|
|
321
|
+
skippedPrdSourceHintSources: countSkippedPrdSourceHintSources(projectRoot, inputs),
|
|
322
|
+
skippedApiSourceHintSources: countSkippedApiSourceHintSources(projectRoot, inputs, cards, ingestSources)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function remainingSourceHintBudget(...groups) {
|
|
326
|
+
const used = groups.reduce((total, group) => total + group.length, 0);
|
|
327
|
+
return Math.max(0, SOURCE_HINT_LIMIT - used);
|
|
328
|
+
}
|
|
329
|
+
function sourceHintSliceLimit(selectedGroups, followingCandidateGroups) {
|
|
330
|
+
const used = selectedGroups.reduce((total, group) => total + group.length, 0);
|
|
331
|
+
const reserved = followingCandidateGroups.filter((group) => group.length > 0).length;
|
|
332
|
+
return Math.max(0, SOURCE_HINT_LIMIT - used - reserved);
|
|
333
|
+
}
|
|
334
|
+
function countSkippedApiSourceHintSources(projectRoot, inputs, cards, ingestSources) {
|
|
335
|
+
const sources = [
|
|
336
|
+
...(inputs.apiSource ? [inputs.apiSource] : []),
|
|
337
|
+
...selectedApiContractSourceCandidates(cards, ingestSources).map((source) => source.apiSource)
|
|
338
|
+
];
|
|
339
|
+
const seen = new Set();
|
|
340
|
+
let skipped = 0;
|
|
341
|
+
for (const source of sources) {
|
|
342
|
+
const normalized = source.replace(/\\/g, "/").toLowerCase();
|
|
343
|
+
if (seen.has(normalized)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
seen.add(normalized);
|
|
347
|
+
if (/^https?:\/\//i.test(source)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const stat = fs.statSync(path.join(projectRoot, source), { throwIfNoEntry: false });
|
|
351
|
+
if (stat?.isFile() && stat.size > MAX_API_SOURCE_HINT_BYTES) {
|
|
352
|
+
skipped += 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return skipped;
|
|
356
|
+
}
|
|
357
|
+
function countSkippedPrdSourceHintSources(projectRoot, inputs) {
|
|
358
|
+
if (!inputs.prd || /^https?:\/\//i.test(inputs.prd)) {
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
const stat = fs.statSync(path.join(projectRoot, inputs.prd), { throwIfNoEntry: false });
|
|
362
|
+
return stat?.isFile() && stat.size > MAX_PRD_SOURCE_HINT_BYTES ? 1 : 0;
|
|
363
|
+
}
|
|
364
|
+
function compactTaskContextLimits(selectedKnowledge, omittedSelectedKnowledge, omittedSelectedKnowledgeSourcePaths, omittedSelectedKnowledgeCoverage, omittedSelectedKnowledgeVerification, omittedSelectedIngestSources, omittedSelectedIngestProducedKnowledgeCardIds, omittedSelectedProjectFiles, omittedSourceHints, omittedPrdCoverageHintCoverage, omittedPrdCoverageHintAnchors, omittedPrdCoverageHintSourcePaths, totalScannedProjectFiles, projectFileScanLimitReached, skippedPrdSourceHintSources, skippedApiSourceHintSources) {
|
|
365
|
+
return {
|
|
366
|
+
selectedKnowledge,
|
|
367
|
+
selectedIngestSources: INGEST_SOURCE_LIMIT,
|
|
368
|
+
selectedProjectFiles: PROJECT_FILE_LIMIT,
|
|
369
|
+
sourceHints: SOURCE_HINT_LIMIT,
|
|
370
|
+
selectedKnowledgeFieldValues: KNOWLEDGE_CARD_FIELD_VALUE_LIMIT,
|
|
371
|
+
selectedIngestProducedKnowledgeCardIds: INGEST_PRODUCED_CARD_ID_LIMIT,
|
|
372
|
+
omittedSelectedKnowledge,
|
|
373
|
+
omittedSelectedKnowledgeSourcePaths,
|
|
374
|
+
omittedSelectedKnowledgeCoverage,
|
|
375
|
+
omittedSelectedKnowledgeVerification,
|
|
376
|
+
omittedSelectedIngestSources,
|
|
377
|
+
omittedSelectedIngestProducedKnowledgeCardIds,
|
|
378
|
+
omittedSelectedProjectFiles,
|
|
379
|
+
omittedSourceHints,
|
|
380
|
+
omittedPrdCoverageHintCoverage,
|
|
381
|
+
omittedPrdCoverageHintAnchors,
|
|
382
|
+
omittedPrdCoverageHintSourcePaths,
|
|
383
|
+
totalScannedProjectFiles,
|
|
384
|
+
projectFileScanLimitReached,
|
|
385
|
+
skippedPrdSourceHintSources,
|
|
386
|
+
skippedApiSourceHintSources,
|
|
387
|
+
scannedProjectFiles: MAX_PROJECT_FILES_SCANNED,
|
|
388
|
+
prdSourceHintBytes: MAX_PRD_SOURCE_HINT_BYTES,
|
|
389
|
+
apiSourceHintBytes: MAX_API_SOURCE_HINT_BYTES
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function selectEndpointHints(projectRoot, task, inputs, cards, ingestSources) {
|
|
393
|
+
const hints = [];
|
|
394
|
+
const hintByKey = new Map();
|
|
395
|
+
for (const card of cards) {
|
|
396
|
+
const text = [
|
|
397
|
+
card.summary,
|
|
398
|
+
card.rule ?? "",
|
|
399
|
+
card.workflow ?? "",
|
|
400
|
+
card.permission ?? "",
|
|
401
|
+
card.note ?? "",
|
|
402
|
+
...card.coverage,
|
|
403
|
+
...card.tags
|
|
404
|
+
].join(" ");
|
|
405
|
+
for (const endpoint of extractEndpointReferences(text)) {
|
|
406
|
+
const key = `${endpoint.method} ${endpoint.path}`;
|
|
407
|
+
if (hintByKey.has(key)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const hint = {
|
|
411
|
+
...endpoint,
|
|
412
|
+
source: card.path,
|
|
413
|
+
cardId: card.id,
|
|
414
|
+
reason: "selected knowledge card"
|
|
415
|
+
};
|
|
416
|
+
hintByKey.set(key, hint);
|
|
417
|
+
hints.push(hint);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const explicitApiIngest = matchingIngestSourceByValue(ingestSources, "openapi", inputs.apiSource);
|
|
421
|
+
for (const endpoint of selectApiSourceEndpointHints(projectRoot, task, inputs.apiSource, explicitApiIngest ? `explicit OpenAPI source metadata; selected ingest provenance ${explicitApiIngest.ingestRef}` : "explicit OpenAPI source metadata", inputs.apiSource, explicitApiIngest?.ingestRef, explicitApiIngest?.artifactPath)) {
|
|
422
|
+
const key = `${endpoint.method} ${endpoint.path}`;
|
|
423
|
+
if (hintByKey.has(key)) {
|
|
424
|
+
const existing = hintByKey.get(key);
|
|
425
|
+
if (existing && endpointHintMatchesAnySource(existing, cards, [inputs.apiSource, explicitApiIngest?.ingestRef])) {
|
|
426
|
+
mergeEndpointHintProvenance(existing, endpoint);
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
hintByKey.set(key, endpoint);
|
|
431
|
+
hints.push(endpoint);
|
|
432
|
+
}
|
|
433
|
+
for (const source of selectedApiContractSourceCandidates(cards, ingestSources)) {
|
|
434
|
+
for (const endpoint of selectApiSourceEndpointHints(projectRoot, task, source.apiSource, "selected API contract knowledge source", source.displaySource, source.ingestRef, source.artifactPath)) {
|
|
435
|
+
const key = `${endpoint.method} ${endpoint.path}`;
|
|
436
|
+
const existing = hintByKey.get(key);
|
|
437
|
+
if (existing) {
|
|
438
|
+
if (endpointHintMatchesAnySource(existing, cards, [source.apiSource, source.displaySource, source.ingestRef])) {
|
|
439
|
+
mergeEndpointHintProvenance(existing, endpoint);
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
hintByKey.set(key, endpoint);
|
|
444
|
+
hints.push(endpoint);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return hints;
|
|
448
|
+
}
|
|
449
|
+
function endpointHintMatchesAnySource(hint, cards, sources) {
|
|
450
|
+
const sourceValues = sources.filter((source) => Boolean(source));
|
|
451
|
+
if (sourceValues.some((source) => sameSourceReference(hint.source, source))) {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
const card = hint.cardId ? cards.find((candidate) => candidate.id === hint.cardId) : undefined;
|
|
455
|
+
return Boolean(card?.sourcePaths.some((sourcePath) => sourceValues.some((source) => sameSourceReference(sourcePath, source))));
|
|
456
|
+
}
|
|
457
|
+
function mergeEndpointHintProvenance(existing, next) {
|
|
458
|
+
if (!existing.ingestRef && next.ingestRef) {
|
|
459
|
+
existing.ingestRef = next.ingestRef;
|
|
460
|
+
}
|
|
461
|
+
if (!existing.artifactPath && next.artifactPath) {
|
|
462
|
+
existing.artifactPath = next.artifactPath;
|
|
463
|
+
}
|
|
464
|
+
if (!existing.reason.includes(next.reason)) {
|
|
465
|
+
existing.reason = `${existing.reason}; ${next.reason}`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function sameSourceReference(left, right) {
|
|
469
|
+
return left.replace(/\\/g, "/").toLowerCase() === right.replace(/\\/g, "/").toLowerCase();
|
|
470
|
+
}
|
|
471
|
+
function extractEndpointReferences(text) {
|
|
472
|
+
const endpoints = [];
|
|
473
|
+
const matches = text.matchAll(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+((?:\/[A-Za-z0-9._~:@!$&'()*+,;=%{}-]+)+)/gi);
|
|
474
|
+
for (const match of matches) {
|
|
475
|
+
const method = match[1]?.toUpperCase();
|
|
476
|
+
const endpointPath = match[2];
|
|
477
|
+
if (method && endpointPath) {
|
|
478
|
+
endpoints.push({ method, path: endpointPath });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return endpoints;
|
|
482
|
+
}
|
|
483
|
+
function selectApiSourceEndpointHints(projectRoot, task, apiSource, reason = "explicit OpenAPI source metadata", displaySource = apiSource, ingestRef, artifactPath) {
|
|
484
|
+
if (!apiSource || /^https?:\/\//i.test(apiSource)) {
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
const fullPath = path.join(projectRoot, apiSource);
|
|
488
|
+
const stat = fs.statSync(fullPath, { throwIfNoEntry: false });
|
|
489
|
+
if (!stat?.isFile() || stat.size > MAX_API_SOURCE_HINT_BYTES) {
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
const sourceText = fs.readFileSync(fullPath, "utf8");
|
|
493
|
+
const endpoints = extractOpenApiEndpointReferences(sourceText);
|
|
494
|
+
const tokens = taskTokens(task);
|
|
495
|
+
const scored = endpoints.map((endpoint, index) => ({
|
|
496
|
+
endpoint,
|
|
497
|
+
index,
|
|
498
|
+
score: scoreText(endpoint.searchText, tokens)
|
|
499
|
+
}));
|
|
500
|
+
return scored
|
|
501
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
502
|
+
.map(({ endpoint: { method, path: endpointPath } }) => ({
|
|
503
|
+
method,
|
|
504
|
+
path: endpointPath,
|
|
505
|
+
source: displaySource ?? apiSource,
|
|
506
|
+
cardId: null,
|
|
507
|
+
reason,
|
|
508
|
+
...(ingestRef ? { ingestRef } : {}),
|
|
509
|
+
...(artifactPath ? { artifactPath } : {})
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
function extractOpenApiEndpointReferences(sourceText) {
|
|
513
|
+
const jsonEndpoints = extractOpenApiJsonEndpointReferences(sourceText);
|
|
514
|
+
if (jsonEndpoints.length) {
|
|
515
|
+
return jsonEndpoints;
|
|
516
|
+
}
|
|
517
|
+
const yamlEndpoints = extractOpenApiYamlEndpointReferences(sourceText);
|
|
518
|
+
return yamlEndpoints.length ? yamlEndpoints : extractSwaggerMarkdownEndpointReferences(sourceText);
|
|
519
|
+
}
|
|
520
|
+
function extractOpenApiJsonEndpointReferences(sourceText) {
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(sourceText);
|
|
523
|
+
if (!parsed.paths || typeof parsed.paths !== "object" || Array.isArray(parsed.paths)) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
const endpoints = [];
|
|
527
|
+
for (const [endpointPath, pathItem] of Object.entries(parsed.paths)) {
|
|
528
|
+
if (!isOpenApiPath(endpointPath) || !pathItem || typeof pathItem !== "object" || Array.isArray(pathItem)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
532
|
+
if (isHttpMethod(method)) {
|
|
533
|
+
endpoints.push({
|
|
534
|
+
method: method.toUpperCase(),
|
|
535
|
+
path: endpointPath,
|
|
536
|
+
searchText: openApiEndpointSearchText(endpointPath, method, operation)
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return endpoints;
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function extractOpenApiYamlEndpointReferences(sourceText) {
|
|
548
|
+
const endpoints = [];
|
|
549
|
+
let currentPath = null;
|
|
550
|
+
let inPaths = false;
|
|
551
|
+
let pathsIndent = -1;
|
|
552
|
+
let pathIndent = -1;
|
|
553
|
+
const lines = sourceText.split(/\r?\n/);
|
|
554
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
555
|
+
const rawLine = lines[index] ?? "";
|
|
556
|
+
const line = rawLine.replace(/#.*/, "");
|
|
557
|
+
if (!line.trim()) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const indent = line.match(/^ */)?.[0].length ?? 0;
|
|
561
|
+
const trimmed = line.trim();
|
|
562
|
+
if (/^paths\s*:\s*$/.test(trimmed)) {
|
|
563
|
+
inPaths = true;
|
|
564
|
+
pathsIndent = indent;
|
|
565
|
+
pathIndent = -1;
|
|
566
|
+
currentPath = null;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!inPaths) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (indent <= pathsIndent) {
|
|
573
|
+
inPaths = false;
|
|
574
|
+
currentPath = null;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const pathMatch = /^["']?(\/[^"':]+(?:\/[^"':]+)*)["']?\s*:\s*$/.exec(trimmed);
|
|
578
|
+
if (pathMatch?.[1]) {
|
|
579
|
+
currentPath = pathMatch[1];
|
|
580
|
+
pathIndent = indent;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const methodMatch = /^([a-zA-Z]+)\s*:\s*$/.exec(trimmed);
|
|
584
|
+
if (currentPath && indent > pathIndent && methodMatch?.[1] && isHttpMethod(methodMatch[1])) {
|
|
585
|
+
endpoints.push({
|
|
586
|
+
method: methodMatch[1].toUpperCase(),
|
|
587
|
+
path: currentPath,
|
|
588
|
+
searchText: openApiYamlEndpointSearchText(lines, index + 1, indent, currentPath, methodMatch[1])
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return endpoints;
|
|
593
|
+
}
|
|
594
|
+
function extractSwaggerMarkdownEndpointReferences(sourceText) {
|
|
595
|
+
const endpoints = [];
|
|
596
|
+
const seen = new Set();
|
|
597
|
+
const endpointBlocks = sourceText.split(/\n\s*\n(?=#{1,6}\s+|##\s*)/);
|
|
598
|
+
const addressPattern = /(?:接口地址|request\s*(?:url|path)|endpoint|path)\s*(?:\*\*)?\s*[::]\s*`?((?:\/[A-Za-z0-9._~:@!$&'()*+,;=%{}-]+)+)`?/i;
|
|
599
|
+
const methodPattern = /(?:请求方式|request\s*method|method)\s*(?:\*\*)?\s*[::]\s*`?\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b`?/i;
|
|
600
|
+
for (const block of endpointBlocks) {
|
|
601
|
+
const pathMatch = addressPattern.exec(block);
|
|
602
|
+
const methodMatch = methodPattern.exec(block);
|
|
603
|
+
const endpointPath = pathMatch?.[1];
|
|
604
|
+
const method = methodMatch?.[1]?.toUpperCase();
|
|
605
|
+
if (!endpointPath || !method) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const key = `${method} ${endpointPath}`;
|
|
609
|
+
if (seen.has(key)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
seen.add(key);
|
|
613
|
+
endpoints.push({
|
|
614
|
+
method,
|
|
615
|
+
path: endpointPath,
|
|
616
|
+
searchText: `${method} ${endpointPath} ${block.slice(0, 2000)}`
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return endpoints;
|
|
620
|
+
}
|
|
621
|
+
function openApiYamlEndpointSearchText(lines, startIndex, methodIndent, endpointPath, method) {
|
|
622
|
+
const values = [method, endpointPath];
|
|
623
|
+
let tagsIndent = -1;
|
|
624
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
625
|
+
const line = (lines[index] ?? "").replace(/#.*/, "");
|
|
626
|
+
if (!line.trim()) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const indent = line.match(/^ */)?.[0].length ?? 0;
|
|
630
|
+
if (indent <= methodIndent) {
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
const trimmed = line.trim();
|
|
634
|
+
const scalarMatch = /^(operationId|summary|description)\s*:\s*(.+?)\s*$/.exec(trimmed);
|
|
635
|
+
if (scalarMatch?.[2]) {
|
|
636
|
+
values.push(unquoteYamlScalar(scalarMatch[2]));
|
|
637
|
+
tagsIndent = -1;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const inlineTagsMatch = /^tags\s*:\s*\[(.*)\]\s*$/.exec(trimmed);
|
|
641
|
+
if (inlineTagsMatch?.[1]) {
|
|
642
|
+
values.push(...parseYamlInlineList(inlineTagsMatch[1]));
|
|
643
|
+
tagsIndent = -1;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (/^tags\s*:\s*$/.test(trimmed)) {
|
|
647
|
+
tagsIndent = indent;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (tagsIndent >= 0) {
|
|
651
|
+
if (indent <= tagsIndent) {
|
|
652
|
+
tagsIndent = -1;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const tagMatch = /^-\s*(.+?)\s*$/.exec(trimmed);
|
|
656
|
+
if (tagMatch?.[1]) {
|
|
657
|
+
values.push(unquoteYamlScalar(tagMatch[1]));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return values.join(" ");
|
|
662
|
+
}
|
|
663
|
+
function parseYamlInlineList(value) {
|
|
664
|
+
return value
|
|
665
|
+
.split(",")
|
|
666
|
+
.map((item) => unquoteYamlScalar(item))
|
|
667
|
+
.filter((item) => item.length > 0);
|
|
668
|
+
}
|
|
669
|
+
function unquoteYamlScalar(value) {
|
|
670
|
+
const trimmed = value.trim();
|
|
671
|
+
const quoted = /^(['"])(.*)\1$/.exec(trimmed);
|
|
672
|
+
return quoted?.[2] ?? trimmed;
|
|
673
|
+
}
|
|
674
|
+
function openApiEndpointSearchText(endpointPath, method, operation) {
|
|
675
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
676
|
+
return `${method} ${endpointPath}`;
|
|
677
|
+
}
|
|
678
|
+
const values = [method, endpointPath];
|
|
679
|
+
for (const field of ["operationId", "summary", "description"]) {
|
|
680
|
+
const value = operation[field];
|
|
681
|
+
if (typeof value === "string") {
|
|
682
|
+
values.push(value);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const tags = operation["tags"];
|
|
686
|
+
if (Array.isArray(tags)) {
|
|
687
|
+
values.push(...tags.filter((tag) => typeof tag === "string"));
|
|
688
|
+
}
|
|
689
|
+
return values.join(" ");
|
|
690
|
+
}
|
|
691
|
+
function isOpenApiPath(value) {
|
|
692
|
+
return value.startsWith("/") && value.length > 1;
|
|
693
|
+
}
|
|
694
|
+
function isHttpMethod(value) {
|
|
695
|
+
return /^(get|post|put|patch|delete|head|options|trace)$/i.test(value);
|
|
696
|
+
}
|
|
697
|
+
function selectPrdCoverageHints(projectRoot, task, inputs, cards, ingestSources) {
|
|
698
|
+
const seen = new Set();
|
|
699
|
+
const selectedHints = prdCoverageHintCards(cards)
|
|
700
|
+
.map((card) => {
|
|
701
|
+
seen.add(card.id);
|
|
702
|
+
return card;
|
|
703
|
+
});
|
|
704
|
+
const explicitPrdHints = selectExplicitPrdCoverageHintCards(projectRoot, task, inputs.prd, ingestSources, seen);
|
|
705
|
+
const cardHints = [...selectedHints, ...explicitPrdHints]
|
|
706
|
+
.slice(0, SOURCE_HINT_LIMIT)
|
|
707
|
+
.map((card) => compactPrdCoverageHint(card, inputs.prd, ingestSources));
|
|
708
|
+
if (cardHints.some((selection) => inputs.prd && selection.hint.sourcePaths.some((sourcePath) => sameSourceReference(sourcePath, inputs.prd ?? "")))) {
|
|
709
|
+
return cardHints;
|
|
710
|
+
}
|
|
711
|
+
return [
|
|
712
|
+
...cardHints,
|
|
713
|
+
...selectExplicitPrdSourceHints(projectRoot, task, inputs.prd, ingestSources)
|
|
714
|
+
].slice(0, SOURCE_HINT_LIMIT);
|
|
715
|
+
}
|
|
716
|
+
function prdCoverageHintCards(cards) {
|
|
717
|
+
return cards
|
|
718
|
+
.filter((card) => card.coverage.length > 0 && card.sourcePaths.some(isPrdContextSource))
|
|
719
|
+
.slice(0, SOURCE_HINT_LIMIT);
|
|
720
|
+
}
|
|
721
|
+
function selectExplicitPrdCoverageHintCards(projectRoot, task, prd, ingestSources, excludedCardIds) {
|
|
722
|
+
if (!prd) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const tokens = taskTokens(task);
|
|
726
|
+
const prdReferences = prdSourceReferences(prd, ingestSources);
|
|
727
|
+
return discoverKnowledgeCards(projectRoot)
|
|
728
|
+
.filter((card) => card.status === "active")
|
|
729
|
+
.filter((card) => !excludedCardIds.has(card.id))
|
|
730
|
+
.filter((card) => card.coverage.length > 0)
|
|
731
|
+
.filter((card) => card.sourcePaths.some((sourcePath) => sourceReferencesInclude(prdReferences, sourcePath)))
|
|
732
|
+
.map((card) => ({
|
|
733
|
+
card,
|
|
734
|
+
score: scoreText([
|
|
735
|
+
card.id,
|
|
736
|
+
card.title,
|
|
737
|
+
card.summary,
|
|
738
|
+
card.subject ?? "",
|
|
739
|
+
card.rule ?? "",
|
|
740
|
+
card.workflow ?? "",
|
|
741
|
+
card.permission ?? "",
|
|
742
|
+
card.term ?? "",
|
|
743
|
+
card.note ?? "",
|
|
744
|
+
card.searchable,
|
|
745
|
+
...card.coverage,
|
|
746
|
+
...card.prdAnchors,
|
|
747
|
+
...card.scope,
|
|
748
|
+
...card.tags
|
|
749
|
+
].join(" "), tokens)
|
|
750
|
+
}))
|
|
751
|
+
.filter((item) => item.score > 0)
|
|
752
|
+
.sort((left, right) => right.score - left.score || left.card.path.localeCompare(right.card.path))
|
|
753
|
+
.slice(0, SOURCE_HINT_LIMIT)
|
|
754
|
+
.map((item) => item.card);
|
|
755
|
+
}
|
|
756
|
+
function prdSourceReferences(prd, ingestSources) {
|
|
757
|
+
const references = new Set([normalizeSourcePath(prd)]);
|
|
758
|
+
addMatchingIngestSourceReferences(references, ingestSources, "prd", prd);
|
|
759
|
+
return references;
|
|
760
|
+
}
|
|
761
|
+
function sourceReferencesInclude(references, sourcePath) {
|
|
762
|
+
for (const reference of references) {
|
|
763
|
+
if (sameSourceReference(sourcePath, reference)) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
function compactPrdCoverageHint(card, explicitPrd, ingestSources) {
|
|
770
|
+
const allSourcePaths = Array.from(new Set([
|
|
771
|
+
...card.sourcePaths.filter((sourcePath) => explicitPrd && sameSourceReference(sourcePath, explicitPrd)),
|
|
772
|
+
...card.sourcePaths.filter(isPrdContextSource)
|
|
773
|
+
]));
|
|
774
|
+
const coverage = card.coverage.slice(0, SOURCE_HINT_LIMIT);
|
|
775
|
+
const anchors = card.prdAnchors.slice(0, SOURCE_HINT_LIMIT);
|
|
776
|
+
const sourcePaths = allSourcePaths.slice(0, SOURCE_HINT_LIMIT);
|
|
777
|
+
const ingest = matchingPrdIngestSource(card, explicitPrd, ingestSources);
|
|
778
|
+
return {
|
|
779
|
+
hint: {
|
|
780
|
+
cardId: card.id,
|
|
781
|
+
coverage,
|
|
782
|
+
anchors,
|
|
783
|
+
sourcePaths,
|
|
784
|
+
...(ingest ? { ingestRef: ingest.ingestRef, artifactPath: ingest.artifactPath } : {})
|
|
785
|
+
},
|
|
786
|
+
omittedCoverage: Math.max(0, card.coverage.length - coverage.length),
|
|
787
|
+
omittedAnchors: Math.max(0, card.prdAnchors.length - anchors.length),
|
|
788
|
+
omittedSourcePaths: Math.max(0, allSourcePaths.length - SOURCE_HINT_LIMIT)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function matchingPrdIngestSource(card, explicitPrd, ingestSources) {
|
|
792
|
+
const explicitSource = explicitPrd ? normalizeSourcePath(explicitPrd) : null;
|
|
793
|
+
return ingestSources.find((record) => record.kind === "prd"
|
|
794
|
+
&& (card.sourcePaths.some((sourcePath) => sameSourceReference(sourcePath, record.ingestRef))
|
|
795
|
+
|| (explicitSource !== null && sameSourceReference(record.source.value, explicitSource) && card.sourcePaths.some((sourcePath) => sameSourceReference(sourcePath, explicitSource))))) ?? null;
|
|
796
|
+
}
|
|
797
|
+
function selectExplicitPrdSourceHints(projectRoot, task, prd, ingestSources) {
|
|
798
|
+
if (!prd || /^https?:\/\//i.test(prd)) {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
const fullPath = path.join(projectRoot, prd);
|
|
802
|
+
const stat = fs.statSync(fullPath, { throwIfNoEntry: false });
|
|
803
|
+
if (!stat?.isFile() || stat.size > MAX_PRD_SOURCE_HINT_BYTES) {
|
|
804
|
+
return [];
|
|
805
|
+
}
|
|
806
|
+
const sourceText = fs.readFileSync(fullPath, "utf8");
|
|
807
|
+
const headingSelection = selectMarkdownHeadingsForTask(sourceText, task);
|
|
808
|
+
const ingest = matchingIngestSourceByValue(ingestSources, "prd", prd);
|
|
809
|
+
return [{
|
|
810
|
+
hint: {
|
|
811
|
+
cardId: null,
|
|
812
|
+
coverage: headingSelection.headings.length ? headingSelection.headings : ["explicit PRD source"],
|
|
813
|
+
anchors: [],
|
|
814
|
+
sourcePaths: [prd],
|
|
815
|
+
...(ingest ? { ingestRef: ingest.ingestRef, artifactPath: ingest.artifactPath } : {})
|
|
816
|
+
},
|
|
817
|
+
omittedCoverage: headingSelection.omitted,
|
|
818
|
+
omittedAnchors: 0,
|
|
819
|
+
omittedSourcePaths: 0
|
|
820
|
+
}];
|
|
821
|
+
}
|
|
822
|
+
function selectMarkdownHeadingsForTask(sourceText, task) {
|
|
823
|
+
const headings = extractMarkdownHeadings(sourceText);
|
|
824
|
+
const tokens = taskTokens(task);
|
|
825
|
+
const scored = headings.map((heading, index) => ({
|
|
826
|
+
heading,
|
|
827
|
+
index,
|
|
828
|
+
score: scoreText(heading, tokens)
|
|
829
|
+
}));
|
|
830
|
+
const hasRelevantHeading = scored.some((item) => item.score > 0);
|
|
831
|
+
const selected = scored
|
|
832
|
+
.sort((left, right) => hasRelevantHeading
|
|
833
|
+
? right.score - left.score || left.index - right.index
|
|
834
|
+
: left.index - right.index)
|
|
835
|
+
.slice(0, SOURCE_HINT_LIMIT)
|
|
836
|
+
.map((item) => item.heading);
|
|
837
|
+
return {
|
|
838
|
+
headings: selected,
|
|
839
|
+
omitted: Math.max(0, headings.length - selected.length)
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function extractMarkdownHeadings(sourceText) {
|
|
843
|
+
const headings = [];
|
|
844
|
+
for (const rawLine of sourceText.split(/\r?\n/)) {
|
|
845
|
+
const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(rawLine.trim());
|
|
846
|
+
if (!match?.[2]) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
headings.push(match[2].replace(/\s+/g, " ").trim());
|
|
850
|
+
if (headings.length >= MAX_PRD_HEADINGS_SCANNED) {
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return headings;
|
|
855
|
+
}
|
|
856
|
+
function selectUiArtifactHints(task, inputs, ingestSources, cards) {
|
|
857
|
+
const hints = [];
|
|
858
|
+
const seen = new Set();
|
|
859
|
+
if (inputs.uiSource) {
|
|
860
|
+
pushUiArtifactHint(hints, seen, {
|
|
861
|
+
kind: inputs.uiSource.kind,
|
|
862
|
+
source: inputs.uiSource.value,
|
|
863
|
+
reason: "explicit UI source input"
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
for (const source of ingestSources) {
|
|
867
|
+
if (source.kind !== "ui-package") {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
pushUiArtifactHint(hints, seen, {
|
|
871
|
+
kind: "ui-package",
|
|
872
|
+
source: source.source.value,
|
|
873
|
+
ingestRef: source.ingestRef,
|
|
874
|
+
artifactPath: source.artifactPath,
|
|
875
|
+
reason: `selected ingest provenance ${source.ingestRef}`
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
const cardHints = [];
|
|
879
|
+
const cardSeen = new Set();
|
|
880
|
+
for (const card of cards) {
|
|
881
|
+
for (const sourcePath of card.sourcePaths) {
|
|
882
|
+
const kind = classifyUiArtifactSource(sourcePath);
|
|
883
|
+
if (!kind) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
pushUiArtifactHint(cardHints, cardSeen, {
|
|
887
|
+
kind,
|
|
888
|
+
source: sourcePath,
|
|
889
|
+
reason: `selected knowledge card ${card.id}`
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const rankedCardHints = cardHints.length > remainingSourceHintBudget(hints)
|
|
894
|
+
? rankUiArtifactHintsForTask(cardHints, task)
|
|
895
|
+
: cardHints;
|
|
896
|
+
for (const hint of rankedCardHints) {
|
|
897
|
+
pushUiArtifactHint(hints, seen, hint);
|
|
898
|
+
}
|
|
899
|
+
const selectedHints = hints.slice(0, SOURCE_HINT_LIMIT);
|
|
900
|
+
return {
|
|
901
|
+
hints: selectedHints,
|
|
902
|
+
omitted: Math.max(0, hints.length - selectedHints.length)
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function rankUiArtifactHintsForTask(hints, task) {
|
|
906
|
+
const tokens = taskTokens(task);
|
|
907
|
+
const scored = hints.map((hint, index) => ({
|
|
908
|
+
hint,
|
|
909
|
+
index,
|
|
910
|
+
score: scoreText(`${hint.source} ${hint.reason}`, tokens)
|
|
911
|
+
}));
|
|
912
|
+
if (!scored.some((item) => item.score > 0)) {
|
|
913
|
+
return hints;
|
|
914
|
+
}
|
|
915
|
+
return scored
|
|
916
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
917
|
+
.map((item) => item.hint);
|
|
918
|
+
}
|
|
919
|
+
function selectApiImplementationHints(projectRoot, task, inputs, cards, ingestSources) {
|
|
920
|
+
const apiSources = apiImplementationSourceCandidates(inputs, cards, ingestSources);
|
|
921
|
+
if (apiSources.length === 0) {
|
|
922
|
+
return { hints: [], omitted: 0 };
|
|
923
|
+
}
|
|
924
|
+
const surface = discoverProjectSurface(projectRoot);
|
|
925
|
+
const files = scanProjectFiles(projectRoot, candidateProjectFileRoots(projectRoot, surface.sourceRoots));
|
|
926
|
+
const hints = [];
|
|
927
|
+
const hintByKey = new Map();
|
|
928
|
+
const seenKeys = new Set();
|
|
929
|
+
let totalHints = 0;
|
|
930
|
+
for (const source of apiSources) {
|
|
931
|
+
const apiTokens = [...new Set([
|
|
932
|
+
...apiSourceTokens(source.apiSource),
|
|
933
|
+
...apiSourceTokens(source.displaySource),
|
|
934
|
+
...apiSourceEndpointTokens(projectRoot, source.apiSource, task)
|
|
935
|
+
])];
|
|
936
|
+
if (apiTokens.length === 0) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const tokens = [...new Set([...taskTokens(task), ...apiTokens])];
|
|
940
|
+
for (const item of files
|
|
941
|
+
.map((file) => ({
|
|
942
|
+
file,
|
|
943
|
+
score: scoreApiImplementationFile(file, tokens, apiTokens)
|
|
944
|
+
}))
|
|
945
|
+
.filter((candidate) => candidate.score > 0)
|
|
946
|
+
.sort((left, right) => right.score - left.score || left.file.localeCompare(right.file))) {
|
|
947
|
+
const key = `${item.file}\0${source.apiSource.replace(/\\/g, "/").toLowerCase()}`;
|
|
948
|
+
const existing = hintByKey.get(key);
|
|
949
|
+
if (existing) {
|
|
950
|
+
if (existing.apiSource.startsWith("ingest://") && !source.displaySource.startsWith("ingest://")) {
|
|
951
|
+
existing.apiSource = source.displaySource;
|
|
952
|
+
}
|
|
953
|
+
if (!existing.reason.includes(source.reason)) {
|
|
954
|
+
existing.reason = `${existing.reason}; ${source.reason}`;
|
|
955
|
+
}
|
|
956
|
+
if (!existing.ingestRef && source.ingestRef) {
|
|
957
|
+
existing.ingestRef = source.ingestRef;
|
|
958
|
+
}
|
|
959
|
+
if (!existing.artifactPath && source.artifactPath) {
|
|
960
|
+
existing.artifactPath = source.artifactPath;
|
|
961
|
+
}
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (seenKeys.has(key)) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
seenKeys.add(key);
|
|
968
|
+
totalHints += 1;
|
|
969
|
+
if (hints.length >= SOURCE_HINT_LIMIT) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const hint = {
|
|
973
|
+
path: item.file,
|
|
974
|
+
apiSource: source.displaySource,
|
|
975
|
+
reason: source.reason,
|
|
976
|
+
...(source.ingestRef ? { ingestRef: source.ingestRef } : {}),
|
|
977
|
+
...(source.artifactPath ? { artifactPath: source.artifactPath } : {})
|
|
978
|
+
};
|
|
979
|
+
hintByKey.set(key, hint);
|
|
980
|
+
hints.push(hint);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
hints,
|
|
985
|
+
omitted: Math.max(0, totalHints - hints.length)
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function apiSourceEndpointTokens(projectRoot, apiSource, task) {
|
|
989
|
+
if (/^https?:\/\//i.test(apiSource)) {
|
|
990
|
+
return [];
|
|
991
|
+
}
|
|
992
|
+
const fullPath = path.join(projectRoot, apiSource);
|
|
993
|
+
const stat = fs.statSync(fullPath, { throwIfNoEntry: false });
|
|
994
|
+
if (!stat?.isFile() || stat.size > MAX_API_SOURCE_HINT_BYTES) {
|
|
995
|
+
return [];
|
|
996
|
+
}
|
|
997
|
+
const sourceText = fs.readFileSync(fullPath, "utf8");
|
|
998
|
+
const taskTokenSet = taskTokens(task);
|
|
999
|
+
const endpoints = extractOpenApiEndpointReferences(sourceText);
|
|
1000
|
+
const relevantEndpoints = endpoints.length === 1
|
|
1001
|
+
? endpoints
|
|
1002
|
+
: endpoints.filter((endpoint) => scoreText(endpoint.searchText, taskTokenSet) > 0);
|
|
1003
|
+
return relevantEndpoints
|
|
1004
|
+
.flatMap((endpoint) => taskTokens(`${endpoint.path} ${endpoint.searchText}`))
|
|
1005
|
+
.filter((token) => !["api", "get", "post", "put", "patch", "delete", "head", "options"].includes(token));
|
|
1006
|
+
}
|
|
1007
|
+
function apiImplementationSourceCandidates(inputs, cards, ingestSources) {
|
|
1008
|
+
const candidates = [];
|
|
1009
|
+
if (inputs.apiSource && !/^https?:\/\//i.test(inputs.apiSource)) {
|
|
1010
|
+
const explicitApiIngest = matchingIngestSourceByValue(ingestSources, "openapi", inputs.apiSource);
|
|
1011
|
+
candidates.push({
|
|
1012
|
+
apiSource: inputs.apiSource,
|
|
1013
|
+
displaySource: inputs.apiSource,
|
|
1014
|
+
reason: explicitApiIngest
|
|
1015
|
+
? `matched explicit API source terms in API/client/service file path; selected ingest provenance ${explicitApiIngest.ingestRef}`
|
|
1016
|
+
: "matched explicit API source terms in API/client/service file path",
|
|
1017
|
+
...(explicitApiIngest ? { ingestRef: explicitApiIngest.ingestRef, artifactPath: explicitApiIngest.artifactPath } : {})
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
for (const source of selectedApiContractSourceCandidates(cards, ingestSources)) {
|
|
1021
|
+
candidates.push({
|
|
1022
|
+
apiSource: source.apiSource,
|
|
1023
|
+
displaySource: source.displaySource,
|
|
1024
|
+
reason: "matched selected API contract source terms in API/client/service file path",
|
|
1025
|
+
...(source.ingestRef ? { ingestRef: source.ingestRef } : {}),
|
|
1026
|
+
...(source.artifactPath ? { artifactPath: source.artifactPath } : {})
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
const seen = new Set();
|
|
1030
|
+
return candidates.filter((candidate) => {
|
|
1031
|
+
if (seen.has(candidate.displaySource)) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
seen.add(candidate.displaySource);
|
|
1035
|
+
return true;
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
function matchingIngestSourceByValue(ingestSources, kind, source) {
|
|
1039
|
+
if (!source) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
return ingestSources.find((record) => record.kind === kind && sameSourceReference(record.source.value, source)) ?? null;
|
|
1043
|
+
}
|
|
1044
|
+
function selectedApiContractSourcePaths(cards) {
|
|
1045
|
+
return [...new Set(cards
|
|
1046
|
+
.filter((card) => card.type === "api_contract")
|
|
1047
|
+
.flatMap((card) => card.sourcePaths)
|
|
1048
|
+
.filter((sourcePath) => sourcePath && !/^https?:\/\//i.test(sourcePath))
|
|
1049
|
+
.map(normalizeSourcePath))];
|
|
1050
|
+
}
|
|
1051
|
+
function selectedApiContractSourceCandidates(cards, ingestSources) {
|
|
1052
|
+
const ingestByRef = new Map(ingestSources.map((record) => [ingestRefKey(record.ingestRef) ?? record.ingestRef, record]));
|
|
1053
|
+
return selectedApiContractSourcePaths(cards).flatMap((sourcePath) => {
|
|
1054
|
+
const refKey = ingestRefKey(sourcePath);
|
|
1055
|
+
if (!refKey) {
|
|
1056
|
+
return [{ apiSource: sourcePath, displaySource: sourcePath }];
|
|
1057
|
+
}
|
|
1058
|
+
const record = ingestByRef.get(refKey);
|
|
1059
|
+
if (record?.kind !== "openapi" || record.source.type !== "project-file") {
|
|
1060
|
+
return [];
|
|
1061
|
+
}
|
|
1062
|
+
return [{ apiSource: record.source.value, displaySource: record.ingestRef, ingestRef: record.ingestRef, artifactPath: record.artifactPath }];
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
function apiSourceTokens(apiSource) {
|
|
1066
|
+
const basename = path.basename(apiSource, path.extname(apiSource)).replace(/[-_.]+/g, " ");
|
|
1067
|
+
return taskTokens(basename)
|
|
1068
|
+
.filter((token) => !["api", "openapi", "swagger", "spec", "schema", "json", "yaml", "yml"].includes(token));
|
|
1069
|
+
}
|
|
1070
|
+
function scoreApiImplementationFile(filePath, tokens, apiTokens) {
|
|
1071
|
+
const normalized = filePath.toLowerCase();
|
|
1072
|
+
if (!/(^|\/)(api|apis|client|clients|service|services|request|requests|http)(\/|[-_.])/i.test(normalized)) {
|
|
1073
|
+
return 0;
|
|
1074
|
+
}
|
|
1075
|
+
const apiTokenScore = scoreText(normalized, apiTokens);
|
|
1076
|
+
if (apiTokenScore === 0) {
|
|
1077
|
+
return 0;
|
|
1078
|
+
}
|
|
1079
|
+
return apiTokenScore + scoreText(normalized, tokens);
|
|
1080
|
+
}
|
|
1081
|
+
function pushUiArtifactHint(hints, seen, hint) {
|
|
1082
|
+
const sourceKey = `source\0${hint.source.replace(/\\/g, "/").toLowerCase()}`;
|
|
1083
|
+
const existing = hints.find((item) => `source\0${item.source.replace(/\\/g, "/").toLowerCase()}` === sourceKey);
|
|
1084
|
+
if (existing) {
|
|
1085
|
+
if (hint.kind === "ui-package") {
|
|
1086
|
+
existing.kind = "ui-package";
|
|
1087
|
+
}
|
|
1088
|
+
if (!existing.ingestRef && hint.ingestRef) {
|
|
1089
|
+
existing.ingestRef = hint.ingestRef;
|
|
1090
|
+
}
|
|
1091
|
+
if (!existing.artifactPath && hint.artifactPath) {
|
|
1092
|
+
existing.artifactPath = hint.artifactPath;
|
|
1093
|
+
}
|
|
1094
|
+
if (!existing.reason.includes(hint.reason)) {
|
|
1095
|
+
existing.reason = `${existing.reason}; ${hint.reason}`;
|
|
1096
|
+
}
|
|
1097
|
+
if (hint.ingestRef) {
|
|
1098
|
+
seen.add(`${hint.kind}\0${ingestRefKey(hint.ingestRef) ?? hint.ingestRef}`);
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const sourceKeyValue = ingestRefKey(hint.source) ?? hint.source;
|
|
1103
|
+
const key = `${hint.kind}\0${sourceKeyValue}`;
|
|
1104
|
+
const ingestKey = hint.ingestRef ? `${hint.kind}\0${ingestRefKey(hint.ingestRef) ?? hint.ingestRef}` : null;
|
|
1105
|
+
if (seen.has(key)) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (ingestKey && seen.has(ingestKey)) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
seen.add(key);
|
|
1112
|
+
seen.add(sourceKey);
|
|
1113
|
+
if (ingestKey) {
|
|
1114
|
+
seen.add(ingestKey);
|
|
1115
|
+
}
|
|
1116
|
+
hints.push(hint);
|
|
1117
|
+
}
|
|
1118
|
+
function classifyUiArtifactSource(sourcePath) {
|
|
1119
|
+
const normalized = sourcePath.toLowerCase();
|
|
1120
|
+
if (normalized.startsWith("ingest://ui-package/")) {
|
|
1121
|
+
return "ui-package";
|
|
1122
|
+
}
|
|
1123
|
+
if (isDesignToolUrl(normalized)) {
|
|
1124
|
+
return "design_url";
|
|
1125
|
+
}
|
|
1126
|
+
if (/^https?:\/\//.test(normalized)) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
const extension = path.extname(normalized);
|
|
1130
|
+
if (isGeneratedMarkupExtension(extension)) {
|
|
1131
|
+
return "generated_markup";
|
|
1132
|
+
}
|
|
1133
|
+
if (isVisualArtifactExtension(extension)) {
|
|
1134
|
+
return "visual_artifact";
|
|
1135
|
+
}
|
|
1136
|
+
if (isDesignArtifactPath(normalized, extension)) {
|
|
1137
|
+
return "design_artifact";
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
function isDesignToolUrl(value) {
|
|
1142
|
+
return externalDesignReference(value) !== null;
|
|
1143
|
+
}
|
|
1144
|
+
function isDesignArtifactPath(value, extension) {
|
|
1145
|
+
if (![".json", ".fig", ".sketch"].includes(extension)) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
return /(^|[\\/._-])(design|ui|mockup|wireframe|prototype|screen|screenshot|figma|pixso|mastergo|stitch)([\\/._-]|$)/i.test(value);
|
|
1149
|
+
}
|
|
1150
|
+
function isPrdContextSource(sourcePath) {
|
|
1151
|
+
const normalized = sourcePath.toLowerCase();
|
|
1152
|
+
return normalized.startsWith("ingest://prd/")
|
|
1153
|
+
|| normalized.includes("/prd/")
|
|
1154
|
+
|| normalized.includes("\\prd\\")
|
|
1155
|
+
|| normalized.endsWith("prd.md")
|
|
1156
|
+
|| normalized.includes("requirement");
|
|
1157
|
+
}
|
|
1158
|
+
function renderPrompt(task, inputs, workflowKind, workflowSummary, stageKinds, cards, ingestSources, projectFiles, sourceHints, limits) {
|
|
1159
|
+
const sourceHintCount = sourceHints.endpoints.length
|
|
1160
|
+
+ sourceHints.prdCoverage.length
|
|
1161
|
+
+ sourceHints.uiArtifacts.length
|
|
1162
|
+
+ sourceHints.apiImplementations.length;
|
|
1163
|
+
return [
|
|
1164
|
+
`Task: ${promptField(task)}`,
|
|
1165
|
+
...(inputs.prd ? [`PRD: ${promptField(inputs.prd)}`] : []),
|
|
1166
|
+
...(inputs.apiSource ? [`API source: ${promptField(inputs.apiSource)}`] : []),
|
|
1167
|
+
...(inputs.uiSource ? [`UI source: ${inputs.uiSource.kind} ${promptField(inputs.uiSource.value)}`] : []),
|
|
1168
|
+
`Workflow: ${workflowKind}`,
|
|
1169
|
+
`Workflow summary: ${workflowSummary}`,
|
|
1170
|
+
`Stages: ${stageKinds.join(" -> ")}`,
|
|
1171
|
+
"",
|
|
1172
|
+
"Relevant knowledge:",
|
|
1173
|
+
...(cards.length
|
|
1174
|
+
? cards.map((card) => `- ${promptField(card.id)}: ${promptField(card.summary)} (${promptField(card.path)})`)
|
|
1175
|
+
: ["- none selected"]),
|
|
1176
|
+
"",
|
|
1177
|
+
"Relevant temporary-source provenance:",
|
|
1178
|
+
...(ingestSources.length
|
|
1179
|
+
? ingestSources.map((source) => `- ${promptField(source.ingestRef)}: ${promptField(source.summary)} (${promptField(source.artifactPath)})`)
|
|
1180
|
+
: ["- none selected"]),
|
|
1181
|
+
"",
|
|
1182
|
+
"Candidate project files:",
|
|
1183
|
+
...(projectFiles.length
|
|
1184
|
+
? projectFiles.map((file) => `- ${promptField(file.path)}: ${promptField(file.reason)}`)
|
|
1185
|
+
: ["- none selected"]),
|
|
1186
|
+
"",
|
|
1187
|
+
"Selected source hints:",
|
|
1188
|
+
...(sourceHints.endpoints.length
|
|
1189
|
+
? sourceHints.endpoints.map((hint) => `- endpoint ${hint.method} ${promptField(hint.path)} from ${promptField(hint.cardId ?? hint.source)}`)
|
|
1190
|
+
: []),
|
|
1191
|
+
...(sourceHints.prdCoverage.length
|
|
1192
|
+
? sourceHints.prdCoverage.map((hint) => `- PRD coverage ${promptField(hint.coverage.join("; "))} from ${promptField(hint.cardId ?? hint.sourcePaths[0] ?? "explicit PRD input")}`)
|
|
1193
|
+
: []),
|
|
1194
|
+
...(sourceHints.uiArtifacts.length
|
|
1195
|
+
? sourceHints.uiArtifacts.map(renderUiArtifactPromptLine)
|
|
1196
|
+
: []),
|
|
1197
|
+
...(sourceHints.apiImplementations.length
|
|
1198
|
+
? sourceHints.apiImplementations.map((hint) => `- API implementation ${promptField(hint.path)} for ${promptField(hint.apiSource)}: ${promptField(hint.reason)}`)
|
|
1199
|
+
: []),
|
|
1200
|
+
...(sourceHints.endpoints.length || sourceHints.prdCoverage.length || sourceHints.uiArtifacts.length || sourceHints.apiImplementations.length
|
|
1201
|
+
? []
|
|
1202
|
+
: ["- none selected"]),
|
|
1203
|
+
"",
|
|
1204
|
+
"Context budget:",
|
|
1205
|
+
`- knowledge ${cards.length}/${limits.selectedKnowledge}; omitted ${limits.omittedSelectedKnowledge}; omitted fields sourcePaths=${limits.omittedSelectedKnowledgeSourcePaths}, coverage=${limits.omittedSelectedKnowledgeCoverage}, verification=${limits.omittedSelectedKnowledgeVerification}`,
|
|
1206
|
+
`- ingest ${ingestSources.length}/${limits.selectedIngestSources}; omitted ${limits.omittedSelectedIngestSources}; omitted producedCardIds=${limits.omittedSelectedIngestProducedKnowledgeCardIds}`,
|
|
1207
|
+
`- project files ${projectFiles.length}/${limits.selectedProjectFiles}; omitted ${limits.omittedSelectedProjectFiles}; scanned ${limits.totalScannedProjectFiles}/${limits.scannedProjectFiles}; scanLimitReached=${limits.projectFileScanLimitReached}`,
|
|
1208
|
+
`- source hints ${sourceHintCount}/${limits.sourceHints}; omitted ${limits.omittedSourceHints}; omittedPrdCoverage coverage=${limits.omittedPrdCoverageHintCoverage}, anchors=${limits.omittedPrdCoverageHintAnchors}, sourcePaths=${limits.omittedPrdCoverageHintSourcePaths}; skippedPrdSources=${limits.skippedPrdSourceHintSources}; skippedApiSources=${limits.skippedApiSourceHintSources}`,
|
|
1209
|
+
"",
|
|
1210
|
+
"Use the selected knowledge as durable project context. Treat ingest records as provenance for already-distilled temporary inputs, not as instructions to re-fetch or paste full source material."
|
|
1211
|
+
].join("\n");
|
|
1212
|
+
}
|
|
1213
|
+
function renderUiArtifactPromptLine(hint) {
|
|
1214
|
+
const prefix = `- UI ${hint.kind} ${promptField(hint.source)}`;
|
|
1215
|
+
return hint.ingestRef
|
|
1216
|
+
? prefix
|
|
1217
|
+
: `${prefix}: ${promptField(hint.reason)}`;
|
|
1218
|
+
}
|
|
1219
|
+
function promptField(value) {
|
|
1220
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
1221
|
+
return normalized.length > MAX_PROMPT_FIELD_CHARS
|
|
1222
|
+
? `${normalized.slice(0, MAX_PROMPT_FIELD_CHARS - 3)}...`
|
|
1223
|
+
: normalized;
|
|
1224
|
+
}
|
|
1225
|
+
function scoreKnowledgeCard(card, tokens, sourceReferences) {
|
|
1226
|
+
const text = [
|
|
1227
|
+
card.id,
|
|
1228
|
+
card.title,
|
|
1229
|
+
card.summary,
|
|
1230
|
+
card.subject ?? "",
|
|
1231
|
+
card.kind ?? "",
|
|
1232
|
+
card.rule ?? "",
|
|
1233
|
+
card.workflow ?? "",
|
|
1234
|
+
card.permission ?? "",
|
|
1235
|
+
card.term ?? "",
|
|
1236
|
+
card.note ?? "",
|
|
1237
|
+
...card.scope,
|
|
1238
|
+
...card.tags,
|
|
1239
|
+
...card.sourcePaths
|
|
1240
|
+
].join(" ");
|
|
1241
|
+
const baseScore = scoreText(text, tokens);
|
|
1242
|
+
const sourcePaths = card.sourcePaths.map(normalizeSourcePath);
|
|
1243
|
+
const prdBoost = sourcePathBoost(sourcePaths, sourceReferences.prd);
|
|
1244
|
+
const apiBoost = sourcePathBoost(sourcePaths, sourceReferences.api);
|
|
1245
|
+
const uiBoost = sourcePathBoost(sourcePaths, sourceReferences.ui);
|
|
1246
|
+
return baseScore + prdBoost + apiBoost + uiBoost;
|
|
1247
|
+
}
|
|
1248
|
+
function sourcePathBoost(sourcePaths, references) {
|
|
1249
|
+
return sourcePaths.some((sourcePath) => sourceReferencesInclude(references, sourcePath)) ? 3 : 0;
|
|
1250
|
+
}
|
|
1251
|
+
function scoreText(text, tokens) {
|
|
1252
|
+
const normalized = text.toLowerCase();
|
|
1253
|
+
const compact = normalized.replace(/[\\/._-]+/g, "");
|
|
1254
|
+
return tokens.reduce((score, token) => score + (normalized.includes(token) || compact.includes(token) ? 1 : 0), 0);
|
|
1255
|
+
}
|
|
1256
|
+
function taskTokens(task) {
|
|
1257
|
+
const normalized = task.toLowerCase();
|
|
1258
|
+
const latin = (normalized.match(/[a-z0-9][a-z0-9_-]{1,}/g) ?? [])
|
|
1259
|
+
.flatMap((token) => {
|
|
1260
|
+
const rawParts = token.split(/[-_]+/).filter((part) => part.length > 1);
|
|
1261
|
+
const parts = rawParts.filter((part) => !SEPARATED_TOKEN_STOP_WORDS.has(part));
|
|
1262
|
+
const compact = rawParts.length > 1 ? rawParts.join("") : "";
|
|
1263
|
+
return [token, compact, ...parts].filter(Boolean);
|
|
1264
|
+
});
|
|
1265
|
+
const chinese = normalized.match(/[\u4e00-\u9fff]{2,}/g) ?? [];
|
|
1266
|
+
return [...new Set([...latin, ...chinese])].slice(0, 40);
|
|
1267
|
+
}
|
|
1268
|
+
function normalizeLimit(value) {
|
|
1269
|
+
if (value === undefined) {
|
|
1270
|
+
return DEFAULT_CONTEXT_CARD_LIMIT;
|
|
1271
|
+
}
|
|
1272
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_CONTEXT_CARD_LIMIT) {
|
|
1273
|
+
throw new Error(`context task --limit must be an integer from 1 to ${MAX_CONTEXT_CARD_LIMIT}`);
|
|
1274
|
+
}
|
|
1275
|
+
return value;
|
|
1276
|
+
}
|
|
1277
|
+
function normalizeTaskPrdReference(projectRoot, source) {
|
|
1278
|
+
return normalizeTaskProjectFileReference(projectRoot, source, "PRD");
|
|
1279
|
+
}
|
|
1280
|
+
function normalizeTaskApiSourceReference(projectRoot, source) {
|
|
1281
|
+
const value = source.trim();
|
|
1282
|
+
if (!value) {
|
|
1283
|
+
throw new Error("context task --api-source cannot be empty");
|
|
1284
|
+
}
|
|
1285
|
+
if (/^https?:\/\//i.test(value)) {
|
|
1286
|
+
return value;
|
|
1287
|
+
}
|
|
1288
|
+
return normalizeTaskProjectFileReference(projectRoot, value, "API source");
|
|
1289
|
+
}
|
|
1290
|
+
function normalizeTaskUiSourceReference(projectRoot, source) {
|
|
1291
|
+
const value = source.trim();
|
|
1292
|
+
if (!value) {
|
|
1293
|
+
throw new Error("context task --ui-source cannot be empty");
|
|
1294
|
+
}
|
|
1295
|
+
if (/^https?:\/\//i.test(value)) {
|
|
1296
|
+
const designUrl = externalDesignReference(value);
|
|
1297
|
+
if (designUrl) {
|
|
1298
|
+
return designUrl;
|
|
1299
|
+
}
|
|
1300
|
+
throw new Error(unsupportedDesignSourceUrlMessage("UI source"));
|
|
1301
|
+
}
|
|
1302
|
+
const normalized = normalizeTaskProjectFileReference(projectRoot, value, "UI source");
|
|
1303
|
+
return {
|
|
1304
|
+
kind: inferLocalUiSourceKind(normalized),
|
|
1305
|
+
value: normalized
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function normalizeTaskProjectFileReference(projectRoot, source, label) {
|
|
1309
|
+
const { relativePath, fullPath } = resolveProjectRelativePath(projectRoot, source, `${label} file`);
|
|
1310
|
+
if (!isRegularFileWithoutSymlink(fullPath)) {
|
|
1311
|
+
throw new Error(`${label} file cannot be read: ${source}`);
|
|
1312
|
+
}
|
|
1313
|
+
return relativePath;
|
|
1314
|
+
}
|
|
1315
|
+
function normalizeSourcePath(value) {
|
|
1316
|
+
return value.replace(/\\/g, "/");
|
|
1317
|
+
}
|
|
1318
|
+
function ingestRefKey(value) {
|
|
1319
|
+
const normalized = normalizeSourcePath(value);
|
|
1320
|
+
return normalized.toLowerCase().startsWith("ingest://") ? normalized.toLowerCase() : null;
|
|
1321
|
+
}
|
|
1322
|
+
//# sourceMappingURL=task-context.js.map
|