frontend-harness 0.7.10 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dist/cli/index.js +399 -65
- package/dist/cli/index.js.map +1 -1
- package/dist/runtime/builtin-skills.js +2 -1
- package/dist/runtime/builtin-skills.js.map +1 -1
- package/dist/runtime/clean.js +1 -0
- package/dist/runtime/clean.js.map +1 -1
- package/dist/runtime/command-taxonomy.js +7 -1
- package/dist/runtime/command-taxonomy.js.map +1 -1
- package/dist/runtime/context.d.ts +33 -2
- package/dist/runtime/context.js +63 -8
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/evidence.d.ts +27 -0
- package/dist/runtime/evidence.js +240 -12
- package/dist/runtime/evidence.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.js +1 -0
- package/dist/runtime/knowledge/constants.js.map +1 -1
- package/dist/runtime/knowledge/core.d.ts +10 -1
- package/dist/runtime/knowledge/core.js +582 -24
- package/dist/runtime/knowledge/core.js.map +1 -1
- package/dist/runtime/knowledge/types.d.ts +125 -1
- package/dist/runtime/knowledge.d.ts +2 -2
- package/dist/runtime/knowledge.js +1 -1
- package/dist/runtime/knowledge.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/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 +132 -79
- package/dist/runtime/plan.js.map +1 -1
- package/dist/runtime/policy-provenance.d.ts +17 -1
- package/dist/runtime/policy-provenance.js +42 -12
- package/dist/runtime/policy-provenance.js.map +1 -1
- package/dist/runtime/protocol-init.js +12 -7
- package/dist/runtime/protocol-init.js.map +1 -1
- package/dist/runtime/repair-packet.js +86 -4
- package/dist/runtime/repair-packet.js.map +1 -1
- package/dist/runtime/skills.d.ts +9 -0
- package/dist/runtime/skills.js +13 -0
- 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 +36 -2
- 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/verify.js +65 -25
- package/dist/runtime/verify.js.map +1 -1
- package/dist/schemas/types.d.ts +75 -2
- package/dist/schemas/validation.js +42 -7
- package/dist/schemas/validation.js.map +1 -1
- package/docs/DIRECTION.md +13 -11
- package/package.json +1 -1
|
@@ -3,7 +3,16 @@ import path from "node:path";
|
|
|
3
3
|
import { isDirectoryWithoutSymlink, isRegularFileWithoutSymlink, writeJson, writeText } from "../../storage/json.js";
|
|
4
4
|
import { harnessPath, relativeHarnessPath } from "../../storage/paths.js";
|
|
5
5
|
import { normalizeProjectRelativePath } from "../project-paths.js";
|
|
6
|
+
import { showIngestRecord } from "../ingest.js";
|
|
6
7
|
import { kindTypeMap, knowledgeKinds, knowledgeStabilities, knowledgeStatuses, knowledgeTypes, MAX_KNOWLEDGE_DIRECTORY_DEPTH, MAX_KNOWLEDGE_MARKDOWN_BYTES, MAX_KNOWLEDGE_MARKDOWN_FILES } from "./constants.js";
|
|
8
|
+
export const MAX_KNOWLEDGE_LIST_CARDS = 20;
|
|
9
|
+
export const MAX_KNOWLEDGE_SEARCH_RESULTS = 8;
|
|
10
|
+
export const MAX_KNOWLEDGE_DRAFT_UPDATE_CANDIDATES = 8;
|
|
11
|
+
export const MAX_KNOWLEDGE_OUTLINE_HEADINGS = 80;
|
|
12
|
+
export const MAX_KNOWLEDGE_DRAFT_EXTRACTION_ITEMS = 40;
|
|
13
|
+
export const MAX_KNOWLEDGE_INDEX_CARDS = 20;
|
|
14
|
+
export const MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS = 20;
|
|
15
|
+
const MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS = 20;
|
|
7
16
|
export function promoteKnowledge(projectRoot, input) {
|
|
8
17
|
const title = input.title?.trim();
|
|
9
18
|
const body = input.body?.trim();
|
|
@@ -121,11 +130,13 @@ export function addKnowledge(projectRoot, input) {
|
|
|
121
130
|
const contentFields = atomicContentFields(kind, input);
|
|
122
131
|
const inputSummary = input.summary?.trim();
|
|
123
132
|
const contentSummary = inputSummary || summarizeAtomicContent(kind, subject, contentFields);
|
|
133
|
+
const sourcePaths = parseKnowledgeSourcePaths(input.source, "knowledge add --source");
|
|
134
|
+
const type = typeForAtomicKnowledge(kind, sourcePaths);
|
|
124
135
|
const rendered = renderAtomicKnowledge({
|
|
125
136
|
id,
|
|
126
137
|
title,
|
|
127
138
|
summary: contentSummary,
|
|
128
|
-
type
|
|
139
|
+
type,
|
|
129
140
|
kind,
|
|
130
141
|
subject,
|
|
131
142
|
status,
|
|
@@ -133,7 +144,7 @@ export function addKnowledge(projectRoot, input) {
|
|
|
133
144
|
createdAt,
|
|
134
145
|
scope: splitList(input.scope),
|
|
135
146
|
tags: splitList(input.tags),
|
|
136
|
-
sourcePaths
|
|
147
|
+
sourcePaths,
|
|
137
148
|
verification: splitList(input.verification),
|
|
138
149
|
coverage: splitList(input.coverage),
|
|
139
150
|
prdAnchors: splitList(input.anchor),
|
|
@@ -144,12 +155,435 @@ export function addKnowledge(projectRoot, input) {
|
|
|
144
155
|
path: relativePath,
|
|
145
156
|
id,
|
|
146
157
|
title,
|
|
147
|
-
type
|
|
158
|
+
type,
|
|
148
159
|
status,
|
|
149
160
|
stability,
|
|
150
161
|
createdAt
|
|
151
162
|
};
|
|
152
163
|
}
|
|
164
|
+
export function outlineKnowledgeFromIngest(projectRoot, input) {
|
|
165
|
+
if (!input.ref?.trim()) {
|
|
166
|
+
throw new Error("knowledge outline requires --ref ingest://<kind>/<source-id>");
|
|
167
|
+
}
|
|
168
|
+
const record = showIngestRecord(projectRoot, input.ref);
|
|
169
|
+
const outlineSelection = buildKnowledgeOutline(projectRoot, record);
|
|
170
|
+
return {
|
|
171
|
+
contractVersion: 1,
|
|
172
|
+
ingestRef: record.ingestRef,
|
|
173
|
+
ingestKind: record.kind,
|
|
174
|
+
source: record.source.value,
|
|
175
|
+
sourceType: record.source.type,
|
|
176
|
+
outline: outlineSelection.headings,
|
|
177
|
+
limits: {
|
|
178
|
+
headings: outlineSelection.limits
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function draftKnowledgeFromIngest(projectRoot, input) {
|
|
183
|
+
if (!input.ref?.trim()) {
|
|
184
|
+
throw new Error("knowledge draft requires --ref ingest://<kind>/<source-id>");
|
|
185
|
+
}
|
|
186
|
+
const record = showIngestRecord(projectRoot, input.ref);
|
|
187
|
+
const recommendedKind = recommendedKnowledgeKind(record.kind);
|
|
188
|
+
const recommendedType = typeForAtomicKnowledge(recommendedKind, [record.ingestRef]);
|
|
189
|
+
const subject = draftSubject(record);
|
|
190
|
+
const id = slugify(`${subject}-${recommendedKind}`) || `${record.id}-${recommendedKind}`;
|
|
191
|
+
const defaults = {
|
|
192
|
+
id,
|
|
193
|
+
kind: recommendedKind,
|
|
194
|
+
subject,
|
|
195
|
+
summary: record.summary,
|
|
196
|
+
source: record.ingestRef,
|
|
197
|
+
coverage: draftCoveragePlaceholder(record.kind),
|
|
198
|
+
verification: draftVerificationPlaceholder(record.kind),
|
|
199
|
+
scope: draftScope(record),
|
|
200
|
+
tags: draftTags(record.kind)
|
|
201
|
+
};
|
|
202
|
+
const contentFlag = contentFlagForKind(recommendedKind);
|
|
203
|
+
const updateCandidateSelection = draftUpdateCandidates(projectRoot, record, defaults);
|
|
204
|
+
const updateCandidates = updateCandidateSelection.candidates;
|
|
205
|
+
const outlineSelection = buildKnowledgeOutline(projectRoot, record);
|
|
206
|
+
const extractionItemSelection = draftExtractionItems(record, outlineSelection.headings, updateCandidates);
|
|
207
|
+
const outlineLimits = outlineSelection.limits.total > 0 ? outlineSelection.limits : undefined;
|
|
208
|
+
const extractionLimits = extractionItemSelection.limits.total > 0 ? extractionItemSelection.limits : undefined;
|
|
209
|
+
const args = [
|
|
210
|
+
"knowledge",
|
|
211
|
+
"add",
|
|
212
|
+
"--json",
|
|
213
|
+
"--id",
|
|
214
|
+
defaults.id,
|
|
215
|
+
"--kind",
|
|
216
|
+
defaults.kind,
|
|
217
|
+
"--subject",
|
|
218
|
+
defaults.subject,
|
|
219
|
+
"--summary",
|
|
220
|
+
defaults.summary,
|
|
221
|
+
`--${contentFlag}`,
|
|
222
|
+
`<atomic ${contentFlag} distilled from ${record.ingestRef}>`,
|
|
223
|
+
"--source",
|
|
224
|
+
defaults.source,
|
|
225
|
+
"--coverage",
|
|
226
|
+
defaults.coverage,
|
|
227
|
+
"--verification",
|
|
228
|
+
defaults.verification,
|
|
229
|
+
"--scope",
|
|
230
|
+
defaults.scope,
|
|
231
|
+
"--tags",
|
|
232
|
+
defaults.tags
|
|
233
|
+
];
|
|
234
|
+
return {
|
|
235
|
+
contractVersion: 1,
|
|
236
|
+
ingestRef: record.ingestRef,
|
|
237
|
+
ingestKind: record.kind,
|
|
238
|
+
source: record.source.value,
|
|
239
|
+
summary: record.summary,
|
|
240
|
+
recommendedKind,
|
|
241
|
+
recommendedType,
|
|
242
|
+
recommendedAction: updateCandidates.length > 0 ? "update" : "add",
|
|
243
|
+
command: "frontend-harness knowledge add --json",
|
|
244
|
+
args,
|
|
245
|
+
updateCandidates,
|
|
246
|
+
...(outlineSelection.headings.length ? { outline: outlineSelection.headings } : {}),
|
|
247
|
+
...(extractionItemSelection.items.length ? { extractionItems: extractionItemSelection.items } : {}),
|
|
248
|
+
limits: {
|
|
249
|
+
updateCandidates: updateCandidateSelection.limits,
|
|
250
|
+
...(outlineLimits ? { outlineHeadings: outlineLimits } : {}),
|
|
251
|
+
...(extractionLimits ? { extractionItems: extractionLimits } : {})
|
|
252
|
+
},
|
|
253
|
+
requiredEdits: [
|
|
254
|
+
...(updateCandidateSelection.limits.total > 0
|
|
255
|
+
? ["Prefer a matching update candidate before adding a new card for the same source."]
|
|
256
|
+
: []),
|
|
257
|
+
...(extractionItemSelection.items.length
|
|
258
|
+
? ["Use extractionItems as the source checklist; add, update, or explicitly skip each required source block."]
|
|
259
|
+
: []),
|
|
260
|
+
`Replace --${contentFlag} with one atomic durable fact; create separate cards for separate facts.`,
|
|
261
|
+
"Replace --coverage with the precise source section, endpoint, screen, or acceptance block.",
|
|
262
|
+
"Replace --verification with the concrete check or evidence that proves the fact still holds.",
|
|
263
|
+
"Narrow --scope and --tags so future context selection can find the card without broad matching."
|
|
264
|
+
],
|
|
265
|
+
defaults
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function draftUpdateCandidates(projectRoot, record, defaults) {
|
|
269
|
+
const producedIds = new Set(record.producedKnowledgeCardIds);
|
|
270
|
+
const cards = discoverKnowledgeCards(projectRoot);
|
|
271
|
+
const matches = cards
|
|
272
|
+
.filter((card) => producedIds.has(card.id) || card.sourcePaths.some((sourcePath) => sameKnowledgeSourceReference(sourcePath, record.ingestRef) || sameKnowledgeSourceReference(sourcePath, record.source.value)))
|
|
273
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
274
|
+
const selected = matches
|
|
275
|
+
.slice(0, MAX_KNOWLEDGE_DRAFT_UPDATE_CANDIDATES)
|
|
276
|
+
.map((card) => ({
|
|
277
|
+
id: card.id,
|
|
278
|
+
path: card.path,
|
|
279
|
+
title: card.title,
|
|
280
|
+
summary: card.summary,
|
|
281
|
+
sourcePaths: card.sourcePaths,
|
|
282
|
+
command: "frontend-harness knowledge update --json",
|
|
283
|
+
args: [
|
|
284
|
+
"knowledge",
|
|
285
|
+
"update",
|
|
286
|
+
"--json",
|
|
287
|
+
"--id",
|
|
288
|
+
card.id,
|
|
289
|
+
"--summary",
|
|
290
|
+
defaults.summary,
|
|
291
|
+
"--source",
|
|
292
|
+
defaults.source,
|
|
293
|
+
"--coverage",
|
|
294
|
+
defaults.coverage,
|
|
295
|
+
"--verification",
|
|
296
|
+
defaults.verification,
|
|
297
|
+
"--scope",
|
|
298
|
+
defaults.scope,
|
|
299
|
+
"--tags",
|
|
300
|
+
defaults.tags
|
|
301
|
+
]
|
|
302
|
+
}));
|
|
303
|
+
return {
|
|
304
|
+
candidates: selected,
|
|
305
|
+
limits: {
|
|
306
|
+
selected: selected.length,
|
|
307
|
+
total: matches.length,
|
|
308
|
+
omitted: Math.max(0, matches.length - selected.length)
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function buildKnowledgeOutline(projectRoot, record) {
|
|
313
|
+
if (record.kind !== "prd" || record.source.type !== "project-file" || !/\.md$/i.test(record.source.value)) {
|
|
314
|
+
return {
|
|
315
|
+
headings: [],
|
|
316
|
+
limits: { selected: 0, total: 0, omitted: 0 }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const fullPath = path.join(projectRoot, record.source.value);
|
|
320
|
+
const content = safeRead(fullPath);
|
|
321
|
+
if (content === null) {
|
|
322
|
+
return {
|
|
323
|
+
headings: [],
|
|
324
|
+
limits: { selected: 0, total: 0, omitted: 0 }
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const headings = parseKnowledgeOutlineHeadings(content);
|
|
328
|
+
const selected = headings.slice(0, MAX_KNOWLEDGE_OUTLINE_HEADINGS);
|
|
329
|
+
return {
|
|
330
|
+
headings: selected,
|
|
331
|
+
limits: {
|
|
332
|
+
selected: selected.length,
|
|
333
|
+
total: headings.length,
|
|
334
|
+
omitted: Math.max(0, headings.length - selected.length)
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function parseKnowledgeOutlineHeadings(content) {
|
|
339
|
+
const lines = content.split(/\r?\n/);
|
|
340
|
+
const rawHeadings = [];
|
|
341
|
+
const stack = [];
|
|
342
|
+
const idCounts = new Map();
|
|
343
|
+
lines.forEach((line, index) => {
|
|
344
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
345
|
+
if (!match?.[1] || !match[2]) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const level = match[1].length;
|
|
349
|
+
const title = match[2].replace(/\s+#+$/, "").trim();
|
|
350
|
+
if (!title) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
while (stack.length && stack[stack.length - 1].level >= level) {
|
|
354
|
+
stack.pop();
|
|
355
|
+
}
|
|
356
|
+
const baseId = slugify(title) || `heading-${index + 1}`;
|
|
357
|
+
const count = idCounts.get(baseId) ?? 0;
|
|
358
|
+
idCounts.set(baseId, count + 1);
|
|
359
|
+
const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
|
|
360
|
+
const parent = stack[stack.length - 1] ?? null;
|
|
361
|
+
const headingPath = [...(parent?.path ?? []), title];
|
|
362
|
+
rawHeadings.push({
|
|
363
|
+
id,
|
|
364
|
+
title,
|
|
365
|
+
level,
|
|
366
|
+
lineStart: index + 1,
|
|
367
|
+
parentId: parent?.id ?? null,
|
|
368
|
+
path: headingPath
|
|
369
|
+
});
|
|
370
|
+
stack.push({ id, level, title, path: headingPath });
|
|
371
|
+
});
|
|
372
|
+
return rawHeadings.map((heading, index) => {
|
|
373
|
+
const nextHeading = rawHeadings.slice(index + 1).find((candidate) => candidate.level <= heading.level);
|
|
374
|
+
const classification = classifyOutlineHeading(heading.title, heading.path);
|
|
375
|
+
return {
|
|
376
|
+
...heading,
|
|
377
|
+
lineEnd: nextHeading ? nextHeading.lineStart - 1 : lines.length,
|
|
378
|
+
extractionRequired: classification.extractionRequired,
|
|
379
|
+
suggestedKind: classification.suggestedKind,
|
|
380
|
+
reason: classification.reason,
|
|
381
|
+
coverage: heading.path.join(" > ")
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function classifyOutlineHeading(title, headingPath) {
|
|
386
|
+
const text = normalize([...headingPath, title].join(" "));
|
|
387
|
+
if (/(?:不做|不扩展|边界|风险|异常|歧义|pitfall|risk|out of scope|non goal|not doing)/i.test(text)) {
|
|
388
|
+
return {
|
|
389
|
+
extractionRequired: true,
|
|
390
|
+
suggestedKind: "pitfall",
|
|
391
|
+
reason: "source block describes boundary, exception, risk, ambiguity, or non-goal information"
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (/(?:权限|按钮|操作|可见|允许|禁止|permission|button|action|visibility)/i.test(text)) {
|
|
395
|
+
return {
|
|
396
|
+
extractionRequired: true,
|
|
397
|
+
suggestedKind: "permission",
|
|
398
|
+
reason: "source block describes action availability, visibility, or permission behavior"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (/(?:流程|链路|流转|workflow|process|journey|flow|lifecycle|状态机)/i.test(text)) {
|
|
402
|
+
return {
|
|
403
|
+
extractionRequired: true,
|
|
404
|
+
suggestedKind: "workflow",
|
|
405
|
+
reason: "source block describes workflow, lifecycle, or state transition semantics"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (/(?:字段|枚举|状态|术语|定义|field|enum|status|term|definition)/i.test(text)) {
|
|
409
|
+
return {
|
|
410
|
+
extractionRequired: true,
|
|
411
|
+
suggestedKind: "term",
|
|
412
|
+
reason: "source block defines fields, states, terms, or enumerations"
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (/(?:模块|页面结构|区域|信息层级|布局|module|section|layout|region)/i.test(text)) {
|
|
416
|
+
return {
|
|
417
|
+
extractionRequired: true,
|
|
418
|
+
suggestedKind: "module_summary",
|
|
419
|
+
reason: "source block describes module, page region, layout, or structure boundaries"
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (/(?:规则|校验|验收|条件|策略|计算|rule|validation|acceptance|criteria|condition|policy)/i.test(text)) {
|
|
423
|
+
return {
|
|
424
|
+
extractionRequired: true,
|
|
425
|
+
suggestedKind: "rule",
|
|
426
|
+
reason: "source block describes durable rule, validation, acceptance, condition, or policy"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
extractionRequired: false,
|
|
431
|
+
suggestedKind: "none",
|
|
432
|
+
reason: "source block appears descriptive; mark skip if it has no durable implementation knowledge"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function draftExtractionItems(record, headings, sourceUpdateCandidates) {
|
|
436
|
+
const requiredHeadings = headings.filter((heading) => heading.extractionRequired && heading.suggestedKind !== "none");
|
|
437
|
+
const items = requiredHeadings
|
|
438
|
+
.slice(0, MAX_KNOWLEDGE_DRAFT_EXTRACTION_ITEMS)
|
|
439
|
+
.map((heading) => draftExtractionItem(record, heading, sourceUpdateCandidates));
|
|
440
|
+
return {
|
|
441
|
+
items,
|
|
442
|
+
limits: {
|
|
443
|
+
selected: items.length,
|
|
444
|
+
total: requiredHeadings.length,
|
|
445
|
+
omitted: Math.max(0, requiredHeadings.length - items.length)
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function draftExtractionItem(record, heading, sourceUpdateCandidates) {
|
|
450
|
+
if (heading.suggestedKind === "none") {
|
|
451
|
+
throw new Error(`cannot draft extraction item for non-required heading: ${heading.id}`);
|
|
452
|
+
}
|
|
453
|
+
const suggestedKind = heading.suggestedKind === "module_summary" ? "module_summary" : heading.suggestedKind;
|
|
454
|
+
const subject = titleCase(heading.title.replace(/^\d+(?:\.\d+)*\s*/, ""));
|
|
455
|
+
const scope = draftScope(record);
|
|
456
|
+
const tags = suggestedKind === "module_summary" ? `prd,module,${scope}` : `prd,${suggestedKind},${scope}`;
|
|
457
|
+
const itemId = slugify(`${record.id}-${heading.title}`) || `${record.id}-${heading.id}`;
|
|
458
|
+
const updateCandidates = sourceUpdateCandidates.filter((candidate) => candidate.title.includes(heading.title) || candidate.summary.includes(heading.title) || candidate.id.includes(slugify(heading.title)));
|
|
459
|
+
if (suggestedKind === "module_summary") {
|
|
460
|
+
return {
|
|
461
|
+
id: itemId,
|
|
462
|
+
headingId: heading.id,
|
|
463
|
+
title: heading.title,
|
|
464
|
+
coverage: heading.coverage,
|
|
465
|
+
suggestedKind,
|
|
466
|
+
extractionRequired: true,
|
|
467
|
+
reason: heading.reason,
|
|
468
|
+
command: "frontend-harness knowledge module --json",
|
|
469
|
+
args: [
|
|
470
|
+
"knowledge",
|
|
471
|
+
"module",
|
|
472
|
+
"--json",
|
|
473
|
+
"--id",
|
|
474
|
+
itemId,
|
|
475
|
+
"--title",
|
|
476
|
+
subject,
|
|
477
|
+
"--summary",
|
|
478
|
+
`<module summary distilled from ${heading.coverage}>`,
|
|
479
|
+
"--body",
|
|
480
|
+
`<structured module facts from ${heading.coverage}>`,
|
|
481
|
+
"--source",
|
|
482
|
+
record.ingestRef,
|
|
483
|
+
"--coverage",
|
|
484
|
+
heading.coverage,
|
|
485
|
+
"--verification",
|
|
486
|
+
"<specific check or evidence required>",
|
|
487
|
+
"--scope",
|
|
488
|
+
scope,
|
|
489
|
+
"--tags",
|
|
490
|
+
tags
|
|
491
|
+
],
|
|
492
|
+
updateCandidates
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const kind = suggestedKind;
|
|
496
|
+
const contentFlag = contentFlagForKind(kind);
|
|
497
|
+
return {
|
|
498
|
+
id: itemId,
|
|
499
|
+
headingId: heading.id,
|
|
500
|
+
title: heading.title,
|
|
501
|
+
coverage: heading.coverage,
|
|
502
|
+
suggestedKind: kind,
|
|
503
|
+
extractionRequired: true,
|
|
504
|
+
reason: heading.reason,
|
|
505
|
+
command: "frontend-harness knowledge add --json",
|
|
506
|
+
args: [
|
|
507
|
+
"knowledge",
|
|
508
|
+
"add",
|
|
509
|
+
"--json",
|
|
510
|
+
"--id",
|
|
511
|
+
itemId,
|
|
512
|
+
"--kind",
|
|
513
|
+
kind,
|
|
514
|
+
"--subject",
|
|
515
|
+
subject,
|
|
516
|
+
"--summary",
|
|
517
|
+
`<summary distilled from ${heading.coverage}>`,
|
|
518
|
+
`--${contentFlag}`,
|
|
519
|
+
`<atomic ${contentFlag} distilled from ${heading.coverage}>`,
|
|
520
|
+
"--source",
|
|
521
|
+
record.ingestRef,
|
|
522
|
+
"--coverage",
|
|
523
|
+
heading.coverage,
|
|
524
|
+
"--verification",
|
|
525
|
+
"<specific check or evidence required>",
|
|
526
|
+
"--scope",
|
|
527
|
+
scope,
|
|
528
|
+
"--tags",
|
|
529
|
+
tags
|
|
530
|
+
],
|
|
531
|
+
updateCandidates
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function recommendedKnowledgeKind(kind) {
|
|
535
|
+
if (kind === "openapi") {
|
|
536
|
+
return "workflow";
|
|
537
|
+
}
|
|
538
|
+
return "rule";
|
|
539
|
+
}
|
|
540
|
+
function typeForAtomicKnowledge(kind, sourcePaths) {
|
|
541
|
+
if (sourcePaths.some(isApiContractSourcePath)) {
|
|
542
|
+
return "api_contract";
|
|
543
|
+
}
|
|
544
|
+
return kindTypeMap[kind];
|
|
545
|
+
}
|
|
546
|
+
function isApiContractSourcePath(sourcePath) {
|
|
547
|
+
const normalized = sourcePath.replace(/\\/g, "/");
|
|
548
|
+
if (/^ingest:\/\/openapi\//i.test(normalized)) {
|
|
549
|
+
return isIngestReference(normalized);
|
|
550
|
+
}
|
|
551
|
+
if (/(^|\/)(openapi|swagger)(\/|[-_.])/i.test(normalized)) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
return /(^|\/)api\/[^/]+\.(?:json|ya?ml)$/i.test(normalized);
|
|
555
|
+
}
|
|
556
|
+
function draftSubject(record) {
|
|
557
|
+
const sourceName = path.basename(record.source.value).replace(/\.[^.]+$/, "");
|
|
558
|
+
const raw = sourceName || record.id;
|
|
559
|
+
return titleCase(raw.replace(/[-_.]+/g, " ")).trim() || record.id;
|
|
560
|
+
}
|
|
561
|
+
function draftScope(record) {
|
|
562
|
+
return record.id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
563
|
+
}
|
|
564
|
+
function draftCoveragePlaceholder(kind) {
|
|
565
|
+
if (kind === "ui-package") {
|
|
566
|
+
return "<screen, component, state, or visual reference>";
|
|
567
|
+
}
|
|
568
|
+
if (kind === "openapi") {
|
|
569
|
+
return "<endpoint, schema, operation, or contract behavior>";
|
|
570
|
+
}
|
|
571
|
+
return "<precise section, endpoint, screen, or acceptance block>";
|
|
572
|
+
}
|
|
573
|
+
function draftVerificationPlaceholder(kind) {
|
|
574
|
+
if (kind === "ui-package") {
|
|
575
|
+
return "<visual, responsive, or interaction evidence required>";
|
|
576
|
+
}
|
|
577
|
+
if (kind === "openapi") {
|
|
578
|
+
return "<contract shape check or mocked integration evidence required>";
|
|
579
|
+
}
|
|
580
|
+
return "<specific check or evidence required>";
|
|
581
|
+
}
|
|
582
|
+
function draftTags(kind) {
|
|
583
|
+
return kind === "ui-package"
|
|
584
|
+
? "ui-package,ui"
|
|
585
|
+
: kind.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
586
|
+
}
|
|
153
587
|
export function updateKnowledge(projectRoot, input) {
|
|
154
588
|
const target = input.id?.trim() || input.path?.trim();
|
|
155
589
|
if (!target) {
|
|
@@ -231,6 +665,7 @@ export function indexKnowledge(projectRoot) {
|
|
|
231
665
|
const cards = discoverKnowledgeCards(projectRoot);
|
|
232
666
|
const prdSources = summarizePrdSources(projectRoot, cards);
|
|
233
667
|
const moduleSummaries = summarizeModules(cards);
|
|
668
|
+
const selectedCards = cards.slice(0, MAX_KNOWLEDGE_INDEX_CARDS);
|
|
234
669
|
const result = {
|
|
235
670
|
artifactPath: relativeHarnessPath("knowledge", "index.json"),
|
|
236
671
|
cardCount: cards.length,
|
|
@@ -242,7 +677,14 @@ export function indexKnowledge(projectRoot) {
|
|
|
242
677
|
byKind: countByKind(cards),
|
|
243
678
|
moduleSummaries,
|
|
244
679
|
prdSources,
|
|
245
|
-
cards:
|
|
680
|
+
cards: selectedCards.map(stripSearchable),
|
|
681
|
+
limits: {
|
|
682
|
+
cards: {
|
|
683
|
+
selected: selectedCards.length,
|
|
684
|
+
total: cards.length,
|
|
685
|
+
omitted: Math.max(0, cards.length - selectedCards.length)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
246
688
|
};
|
|
247
689
|
writeJson(path.join(projectRoot, result.artifactPath), result);
|
|
248
690
|
return result;
|
|
@@ -270,7 +712,7 @@ export function checkKnowledgeCoverage(projectRoot, options = {}) {
|
|
|
270
712
|
.filter((source) => source.activeCardCount === 0)
|
|
271
713
|
.map((source) => source.path);
|
|
272
714
|
const missingSourcePaths = [...new Set(cards.flatMap((card) => card.sourcePaths)
|
|
273
|
-
.filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !
|
|
715
|
+
.filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)))].sort();
|
|
274
716
|
const cardsWithoutSource = cards
|
|
275
717
|
.filter((card) => card.status === "active" && card.type === "prd_semantics" && card.sourcePaths.length === 0)
|
|
276
718
|
.map((card) => card.path);
|
|
@@ -320,15 +762,23 @@ export function listKnowledge(projectRoot, options = {}) {
|
|
|
320
762
|
const cards = discoverKnowledgeCards(projectRoot)
|
|
321
763
|
.filter((card) => options.includeDeprecated || card.status === "active")
|
|
322
764
|
.map(stripSearchable);
|
|
323
|
-
|
|
765
|
+
const selected = cards.slice(0, MAX_KNOWLEDGE_LIST_CARDS);
|
|
766
|
+
return {
|
|
767
|
+
cards: selected,
|
|
768
|
+
limits: {
|
|
769
|
+
selected: selected.length,
|
|
770
|
+
total: cards.length,
|
|
771
|
+
omitted: Math.max(0, cards.length - selected.length)
|
|
772
|
+
}
|
|
773
|
+
};
|
|
324
774
|
}
|
|
325
775
|
export function searchKnowledge(projectRoot, query, options = {}) {
|
|
326
776
|
const normalizedQuery = normalize(query ?? "");
|
|
327
777
|
if (!normalizedQuery) {
|
|
328
778
|
throw new Error("knowledge search requires --query");
|
|
329
779
|
}
|
|
330
|
-
const limit = options.limit && options.limit > 0 ? options.limit :
|
|
331
|
-
const
|
|
780
|
+
const limit = options.limit && options.limit > 0 ? options.limit : MAX_KNOWLEDGE_SEARCH_RESULTS;
|
|
781
|
+
const matches = discoverKnowledgeCards(projectRoot)
|
|
332
782
|
.filter((card) => options.includeDeprecated || card.status === "active")
|
|
333
783
|
.map((card) => ({
|
|
334
784
|
card,
|
|
@@ -340,13 +790,22 @@ export function searchKnowledge(projectRoot, query, options = {}) {
|
|
|
340
790
|
return right.score - left.score;
|
|
341
791
|
}
|
|
342
792
|
return left.card.path.localeCompare(right.card.path);
|
|
343
|
-
})
|
|
793
|
+
});
|
|
794
|
+
const cards = matches
|
|
344
795
|
.slice(0, limit)
|
|
345
796
|
.map(({ card, score }) => ({
|
|
346
797
|
...stripSearchable(card),
|
|
347
798
|
score
|
|
348
799
|
}));
|
|
349
|
-
return {
|
|
800
|
+
return {
|
|
801
|
+
query: query ?? "",
|
|
802
|
+
cards,
|
|
803
|
+
limits: {
|
|
804
|
+
selected: cards.length,
|
|
805
|
+
total: matches.length,
|
|
806
|
+
omitted: Math.max(0, matches.length - cards.length)
|
|
807
|
+
}
|
|
808
|
+
};
|
|
350
809
|
}
|
|
351
810
|
export function showKnowledge(projectRoot, idOrPath) {
|
|
352
811
|
const target = idOrPath?.trim();
|
|
@@ -368,6 +827,9 @@ export function showKnowledge(projectRoot, idOrPath) {
|
|
|
368
827
|
body: extractMarkdownBody(content)
|
|
369
828
|
};
|
|
370
829
|
}
|
|
830
|
+
function sameKnowledgeSourceReference(left, right) {
|
|
831
|
+
return left.replace(/\\/g, "/").toLowerCase() === right.replace(/\\/g, "/").toLowerCase();
|
|
832
|
+
}
|
|
371
833
|
export function checkKnowledge(projectRoot) {
|
|
372
834
|
const knowledgeRoot = harnessPath(projectRoot, "knowledge");
|
|
373
835
|
const relativeRoot = path.relative(projectRoot, knowledgeRoot).split(path.sep).join(path.posix.sep);
|
|
@@ -431,6 +893,15 @@ export function checkKnowledge(projectRoot) {
|
|
|
431
893
|
if (contentValue && normalize(contentValue) === normalize(parsed.metadata.summary ?? "")) {
|
|
432
894
|
warnings.push(`${relativePath} summary duplicates the primary atomic content; use summary for context and the primary field for the durable rule.`);
|
|
433
895
|
}
|
|
896
|
+
if (parsed.metadata.status === "active" && parsed.metadata.sourcePaths.some((sourcePath) => sourcePath.toLowerCase().startsWith("ingest://"))) {
|
|
897
|
+
errors.push(...ingestLinkedAtomicMetadataErrors(relativePath, parsed.metadata));
|
|
898
|
+
}
|
|
899
|
+
for (const coverage of parsed.metadata.coverage.filter(isDraftPlaceholderCoverage)) {
|
|
900
|
+
errors.push(`${relativePath} coverage contains an unedited knowledge draft placeholder: ${coverage}.`);
|
|
901
|
+
}
|
|
902
|
+
for (const verification of parsed.metadata.verification.filter(isDraftPlaceholderVerification)) {
|
|
903
|
+
errors.push(`${relativePath} verification contains an unedited knowledge draft placeholder: ${verification}.`);
|
|
904
|
+
}
|
|
434
905
|
if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.length === 0) {
|
|
435
906
|
warnings.push(`${relativePath} should include coverage items for PRD block traceability.`);
|
|
436
907
|
}
|
|
@@ -442,7 +913,7 @@ export function checkKnowledge(projectRoot) {
|
|
|
442
913
|
warnings.push(`${relativePath} frontmatter coverage disagrees with body traceability.`);
|
|
443
914
|
}
|
|
444
915
|
for (const sourcePath of parsed.metadata.sourcePaths) {
|
|
445
|
-
if (sourcePath && !isExternalReference(sourcePath) && !
|
|
916
|
+
if (sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)) {
|
|
446
917
|
warnings.push(`${relativePath} source path is missing or unsafe: ${sourcePath}.`);
|
|
447
918
|
}
|
|
448
919
|
}
|
|
@@ -546,17 +1017,22 @@ function resolveKnowledgeCardReference(cards, target) {
|
|
|
546
1017
|
return idMatches[0] ?? null;
|
|
547
1018
|
}
|
|
548
1019
|
if (idMatches.length > 1) {
|
|
549
|
-
throw new Error(
|
|
1020
|
+
throw new Error(formatAmbiguousKnowledgeReference(target, idMatches));
|
|
550
1021
|
}
|
|
551
1022
|
const pathSuffixMatches = cards.filter((item) => item.path.endsWith(`/${target}`));
|
|
552
1023
|
if (pathSuffixMatches.length === 1) {
|
|
553
1024
|
return pathSuffixMatches[0] ?? null;
|
|
554
1025
|
}
|
|
555
1026
|
if (pathSuffixMatches.length > 1) {
|
|
556
|
-
throw new Error(
|
|
1027
|
+
throw new Error(formatAmbiguousKnowledgeReference(target, pathSuffixMatches));
|
|
557
1028
|
}
|
|
558
1029
|
return null;
|
|
559
1030
|
}
|
|
1031
|
+
function formatAmbiguousKnowledgeReference(target, matches) {
|
|
1032
|
+
const selected = matches.slice(0, MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS).map((item) => item.path);
|
|
1033
|
+
const omitted = Math.max(0, matches.length - selected.length);
|
|
1034
|
+
return `knowledge card reference is ambiguous: ${target}. Use --path with one of (selected: ${selected.length}; total: ${matches.length}; omitted: ${omitted}): ${selected.join(", ")}.`;
|
|
1035
|
+
}
|
|
560
1036
|
function parseKnowledgeFrontmatter(content) {
|
|
561
1037
|
const metadata = {
|
|
562
1038
|
scope: [],
|
|
@@ -932,6 +1408,9 @@ function validateKnowledgeSourcePath(value) {
|
|
|
932
1408
|
if (!value.trim()) {
|
|
933
1409
|
return "empty";
|
|
934
1410
|
}
|
|
1411
|
+
if (value.toLowerCase().startsWith("ingest://")) {
|
|
1412
|
+
return isIngestReference(value) ? null : "unsafe";
|
|
1413
|
+
}
|
|
935
1414
|
if (isExternalReference(value)) {
|
|
936
1415
|
return null;
|
|
937
1416
|
}
|
|
@@ -946,6 +1425,13 @@ function validateKnowledgeSourcePath(value) {
|
|
|
946
1425
|
function isKnowledgeSourceFile(projectRoot, sourcePath) {
|
|
947
1426
|
return isRegularFileWithoutSymlink(path.join(projectRoot, sourcePath));
|
|
948
1427
|
}
|
|
1428
|
+
function isResolvedKnowledgeSource(projectRoot, sourcePath) {
|
|
1429
|
+
if (isIngestReference(sourcePath)) {
|
|
1430
|
+
const parsed = parseIngestReference(sourcePath);
|
|
1431
|
+
return parsed !== null && isRegularFileWithoutSymlink(harnessPath(projectRoot, "ingest", parsed.kind, `${parsed.id}.json`));
|
|
1432
|
+
}
|
|
1433
|
+
return isKnowledgeSourceFile(projectRoot, sourcePath);
|
|
1434
|
+
}
|
|
949
1435
|
function mergeList(existing, next) {
|
|
950
1436
|
if (next === undefined) {
|
|
951
1437
|
return existing;
|
|
@@ -1142,11 +1628,29 @@ function extractMarkdownBody(content) {
|
|
|
1142
1628
|
return content.slice(endIndex).replace(/^\r?\n---\r?\n?/, "").trim();
|
|
1143
1629
|
}
|
|
1144
1630
|
function isExternalReference(value) {
|
|
1631
|
+
if (value.toLowerCase().startsWith("ingest://")) {
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1145
1634
|
if (/^[A-Za-z]:/.test(value)) {
|
|
1146
1635
|
return false;
|
|
1147
1636
|
}
|
|
1148
1637
|
return /^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
1149
1638
|
}
|
|
1639
|
+
function isIngestReference(value) {
|
|
1640
|
+
return /^ingest:\/\/(?:prd|openapi|ui-package)\/[a-z0-9][a-z0-9._-]{1,80}$/i.test(value)
|
|
1641
|
+
&& !value.includes("..")
|
|
1642
|
+
&& !/[?#]/.test(value);
|
|
1643
|
+
}
|
|
1644
|
+
function parseIngestReference(value) {
|
|
1645
|
+
const match = /^ingest:\/\/(prd|openapi|ui-package)\/([a-z0-9][a-z0-9._-]{1,80})$/i.exec(value);
|
|
1646
|
+
if (!match?.[1] || !match[2]) {
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
kind: match[1].toLowerCase(),
|
|
1651
|
+
id: match[2]
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1150
1654
|
function isBroadCoverageItem(value) {
|
|
1151
1655
|
const normalized = value.trim().toLowerCase();
|
|
1152
1656
|
if (!normalized) {
|
|
@@ -1167,6 +1671,35 @@ function hasCoverageBodyDrift(projectRoot, card) {
|
|
|
1167
1671
|
const content = safeRead(path.join(projectRoot, card.path));
|
|
1168
1672
|
return content !== null && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content));
|
|
1169
1673
|
}
|
|
1674
|
+
function isDraftPlaceholderCoverage(value) {
|
|
1675
|
+
const normalized = value.trim().toLowerCase();
|
|
1676
|
+
return (normalized.startsWith("<") || normalized.endsWith(">"))
|
|
1677
|
+
&& (normalized.includes("precise section")
|
|
1678
|
+
|| normalized.includes("acceptance block")
|
|
1679
|
+
|| normalized === "<precise"
|
|
1680
|
+
|| normalized === "or acceptance block>");
|
|
1681
|
+
}
|
|
1682
|
+
function isDraftPlaceholderVerification(value) {
|
|
1683
|
+
const normalized = value.trim().toLowerCase();
|
|
1684
|
+
return /^<.*>$/.test(normalized)
|
|
1685
|
+
&& normalized.includes("specific")
|
|
1686
|
+
&& (normalized.includes("check") || normalized.includes("evidence"));
|
|
1687
|
+
}
|
|
1688
|
+
function ingestLinkedAtomicMetadataErrors(relativePath, metadata) {
|
|
1689
|
+
const errors = [];
|
|
1690
|
+
const requiredLists = [
|
|
1691
|
+
["scope", metadata.scope],
|
|
1692
|
+
["tags", metadata.tags],
|
|
1693
|
+
["coverage", metadata.coverage],
|
|
1694
|
+
["verification", metadata.verification]
|
|
1695
|
+
];
|
|
1696
|
+
for (const [field, values] of requiredLists) {
|
|
1697
|
+
if (values.length === 0) {
|
|
1698
|
+
errors.push(`${relativePath} active ingest-linked atomic knowledge must include ${field}.`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return errors;
|
|
1702
|
+
}
|
|
1170
1703
|
function summarizePrdSources(projectRoot, cards, requiredSources = []) {
|
|
1171
1704
|
const grouped = new Map();
|
|
1172
1705
|
for (const sourcePath of discoverPrdSourceFiles(projectRoot)) {
|
|
@@ -1182,16 +1715,30 @@ function summarizePrdSources(projectRoot, cards, requiredSources = []) {
|
|
|
1182
1715
|
}
|
|
1183
1716
|
return [...grouped.entries()]
|
|
1184
1717
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1185
|
-
.map(([sourcePath, sourceCards]) =>
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1718
|
+
.map(([sourcePath, sourceCards]) => {
|
|
1719
|
+
const cardIds = sourceCards.map((card) => card.id).sort();
|
|
1720
|
+
const scopes = [...new Set(sourceCards.flatMap((card) => card.scope))].sort();
|
|
1721
|
+
const tags = [...new Set(sourceCards.flatMap((card) => card.tags))].sort();
|
|
1722
|
+
const coverage = [...new Set(sourceCards.flatMap((card) => card.coverage))].sort();
|
|
1723
|
+
const prdAnchors = [...new Set(sourceCards.flatMap((card) => card.prdAnchors))].sort();
|
|
1724
|
+
return {
|
|
1725
|
+
path: sourcePath,
|
|
1726
|
+
cardCount: sourceCards.length,
|
|
1727
|
+
activeCardCount: sourceCards.filter((card) => card.status === "active").length,
|
|
1728
|
+
cardIds: selectedIndexDetails(cardIds),
|
|
1729
|
+
scopes: selectedIndexDetails(scopes),
|
|
1730
|
+
tags: selectedIndexDetails(tags),
|
|
1731
|
+
coverage: selectedIndexDetails(coverage),
|
|
1732
|
+
prdAnchors: selectedIndexDetails(prdAnchors),
|
|
1733
|
+
omitted: {
|
|
1734
|
+
cardIds: omittedIndexDetails(cardIds),
|
|
1735
|
+
scopes: omittedIndexDetails(scopes),
|
|
1736
|
+
tags: omittedIndexDetails(tags),
|
|
1737
|
+
coverage: omittedIndexDetails(coverage),
|
|
1738
|
+
prdAnchors: omittedIndexDetails(prdAnchors)
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
});
|
|
1195
1742
|
}
|
|
1196
1743
|
function summarizeModules(cards) {
|
|
1197
1744
|
const atomicCards = cards.filter((card) => card.kind && card.status === "active");
|
|
@@ -1204,6 +1751,7 @@ function summarizeModules(cards) {
|
|
|
1204
1751
|
card.related.includes(moduleCard.id) ||
|
|
1205
1752
|
card.sourcePaths.some((sourcePath) => moduleSources.has(sourcePath)) ||
|
|
1206
1753
|
card.scope.some((scope) => moduleCard.scope.includes(scope)));
|
|
1754
|
+
const atomicCardIds = relatedAtomicCards.map((card) => card.id).sort();
|
|
1207
1755
|
return {
|
|
1208
1756
|
id: moduleCard.id,
|
|
1209
1757
|
path: moduleCard.path,
|
|
@@ -1214,11 +1762,18 @@ function summarizeModules(cards) {
|
|
|
1214
1762
|
tags: moduleCard.tags,
|
|
1215
1763
|
related: moduleCard.related,
|
|
1216
1764
|
atomicCardCount: relatedAtomicCards.length,
|
|
1217
|
-
atomicCardIds:
|
|
1765
|
+
atomicCardIds: selectedIndexDetails(atomicCardIds),
|
|
1766
|
+
omittedAtomicCardIds: omittedIndexDetails(atomicCardIds)
|
|
1218
1767
|
};
|
|
1219
1768
|
})
|
|
1220
1769
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1221
1770
|
}
|
|
1771
|
+
function selectedIndexDetails(values) {
|
|
1772
|
+
return values.slice(0, MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS);
|
|
1773
|
+
}
|
|
1774
|
+
function omittedIndexDetails(values) {
|
|
1775
|
+
return Math.max(0, values.length - MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS);
|
|
1776
|
+
}
|
|
1222
1777
|
function discoverPrdSourceFiles(projectRoot) {
|
|
1223
1778
|
const roots = ["docs/prd", "docs/requirements", "prd", "requirements"];
|
|
1224
1779
|
const rootSources = roots.flatMap((root) => {
|
|
@@ -1237,6 +1792,9 @@ function discoverPrdSourceFiles(projectRoot) {
|
|
|
1237
1792
|
}
|
|
1238
1793
|
function isPrdSourcePath(sourcePath) {
|
|
1239
1794
|
const normalized = sourcePath.replace(/\\/g, "/");
|
|
1795
|
+
if (/^ingest:\/\/prd\//i.test(normalized)) {
|
|
1796
|
+
return isIngestReference(normalized);
|
|
1797
|
+
}
|
|
1240
1798
|
if (/(^|\/)(prd|requirements?)(\/|[-_.])|(^|\/)docs\/(prd|requirements?)(\/|$)/i.test(normalized)) {
|
|
1241
1799
|
return true;
|
|
1242
1800
|
}
|