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.
Files changed (98) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +20 -10
  3. package/dist/cli/index.js +388 -66
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/runtime/builtin-skills.js +1 -1
  6. package/dist/runtime/clean.js +25 -3
  7. package/dist/runtime/clean.js.map +1 -1
  8. package/dist/runtime/command-taxonomy.js +6 -1
  9. package/dist/runtime/command-taxonomy.js.map +1 -1
  10. package/dist/runtime/context.d.ts +33 -2
  11. package/dist/runtime/context.js +112 -13
  12. package/dist/runtime/context.js.map +1 -1
  13. package/dist/runtime/evidence.d.ts +27 -0
  14. package/dist/runtime/evidence.js +302 -19
  15. package/dist/runtime/evidence.js.map +1 -1
  16. package/dist/runtime/graph.js +11 -4
  17. package/dist/runtime/graph.js.map +1 -1
  18. package/dist/runtime/ingest.d.ts +38 -0
  19. package/dist/runtime/ingest.js +157 -0
  20. package/dist/runtime/ingest.js.map +1 -0
  21. package/dist/runtime/knowledge/constants.d.ts +9 -0
  22. package/dist/runtime/knowledge/constants.js +25 -0
  23. package/dist/runtime/knowledge/constants.js.map +1 -0
  24. package/dist/runtime/knowledge/core.d.ts +25 -0
  25. package/dist/runtime/knowledge/core.js +1569 -0
  26. package/dist/runtime/knowledge/core.js.map +1 -0
  27. package/dist/runtime/knowledge/types.d.ts +241 -0
  28. package/dist/runtime/knowledge/types.js +2 -0
  29. package/dist/runtime/knowledge/types.js.map +1 -0
  30. package/dist/runtime/knowledge.d.ts +2 -188
  31. package/dist/runtime/knowledge.js +1 -1126
  32. package/dist/runtime/knowledge.js.map +1 -1
  33. package/dist/runtime/plan/component-resolver.js +17 -6
  34. package/dist/runtime/plan/component-resolver.js.map +1 -1
  35. package/dist/runtime/plan/guidance.d.ts +1 -1
  36. package/dist/runtime/plan/guidance.js +88 -25
  37. package/dist/runtime/plan/guidance.js.map +1 -1
  38. package/dist/runtime/plan/proposal.js +9 -0
  39. package/dist/runtime/plan/proposal.js.map +1 -1
  40. package/dist/runtime/plan/workflow.d.ts +1 -0
  41. package/dist/runtime/plan/workflow.js +32 -1
  42. package/dist/runtime/plan/workflow.js.map +1 -1
  43. package/dist/runtime/plan.d.ts +13 -0
  44. package/dist/runtime/plan.js +137 -82
  45. package/dist/runtime/plan.js.map +1 -1
  46. package/dist/runtime/policy-provenance.d.ts +17 -1
  47. package/dist/runtime/policy-provenance.js +84 -16
  48. package/dist/runtime/policy-provenance.js.map +1 -1
  49. package/dist/runtime/project-discovery.js +32 -17
  50. package/dist/runtime/project-discovery.js.map +1 -1
  51. package/dist/runtime/project-paths.js +7 -1
  52. package/dist/runtime/project-paths.js.map +1 -1
  53. package/dist/runtime/protocol-init.js +117 -26
  54. package/dist/runtime/protocol-init.js.map +1 -1
  55. package/dist/runtime/repair-decision.js +76 -16
  56. package/dist/runtime/repair-decision.js.map +1 -1
  57. package/dist/runtime/repair-packet.js +141 -20
  58. package/dist/runtime/repair-packet.js.map +1 -1
  59. package/dist/runtime/scaffold/vue-template.js +1 -1
  60. package/dist/runtime/scaffold/vue-template.js.map +1 -1
  61. package/dist/runtime/scaffold.js +44 -8
  62. package/dist/runtime/scaffold.js.map +1 -1
  63. package/dist/runtime/skills.d.ts +9 -0
  64. package/dist/runtime/skills.js +133 -18
  65. package/dist/runtime/skills.js.map +1 -1
  66. package/dist/runtime/state-explain.js +39 -7
  67. package/dist/runtime/state-explain.js.map +1 -1
  68. package/dist/runtime/state.js +85 -10
  69. package/dist/runtime/state.js.map +1 -1
  70. package/dist/runtime/task-context.d.ts +122 -0
  71. package/dist/runtime/task-context.js +1322 -0
  72. package/dist/runtime/task-context.js.map +1 -0
  73. package/dist/runtime/task-signals.d.ts +1 -0
  74. package/dist/runtime/task-signals.js +1 -1
  75. package/dist/runtime/task-signals.js.map +1 -1
  76. package/dist/runtime/ui-restoration.d.ts +3 -3
  77. package/dist/runtime/ui-restoration.js +3 -3
  78. package/dist/runtime/ui-restoration.js.map +1 -1
  79. package/dist/runtime/ui-source.d.ts +6 -0
  80. package/dist/runtime/ui-source.js +66 -0
  81. package/dist/runtime/ui-source.js.map +1 -0
  82. package/dist/runtime/units.js +11 -4
  83. package/dist/runtime/units.js.map +1 -1
  84. package/dist/runtime/verification-commands.js +95 -21
  85. package/dist/runtime/verification-commands.js.map +1 -1
  86. package/dist/runtime/verify.js +129 -43
  87. package/dist/runtime/verify.js.map +1 -1
  88. package/dist/runtime/visual-compare.js +104 -12
  89. package/dist/runtime/visual-compare.js.map +1 -1
  90. package/dist/schemas/types.d.ts +75 -2
  91. package/dist/schemas/validation.js +257 -6
  92. package/dist/schemas/validation.js.map +1 -1
  93. package/dist/storage/json.d.ts +9 -1
  94. package/dist/storage/json.js +106 -8
  95. package/dist/storage/json.js.map +1 -1
  96. package/docs/DIRECTION.md +13 -11
  97. package/docs/RELEASE.md +5 -1
  98. 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