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.
Files changed (69) hide show
  1. package/README.md +18 -10
  2. package/dist/cli/index.js +399 -65
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/runtime/builtin-skills.js +2 -1
  5. package/dist/runtime/builtin-skills.js.map +1 -1
  6. package/dist/runtime/clean.js +1 -0
  7. package/dist/runtime/clean.js.map +1 -1
  8. package/dist/runtime/command-taxonomy.js +7 -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 +63 -8
  12. package/dist/runtime/context.js.map +1 -1
  13. package/dist/runtime/evidence.d.ts +27 -0
  14. package/dist/runtime/evidence.js +240 -12
  15. package/dist/runtime/evidence.js.map +1 -1
  16. package/dist/runtime/ingest.d.ts +38 -0
  17. package/dist/runtime/ingest.js +157 -0
  18. package/dist/runtime/ingest.js.map +1 -0
  19. package/dist/runtime/knowledge/constants.js +1 -0
  20. package/dist/runtime/knowledge/constants.js.map +1 -1
  21. package/dist/runtime/knowledge/core.d.ts +10 -1
  22. package/dist/runtime/knowledge/core.js +582 -24
  23. package/dist/runtime/knowledge/core.js.map +1 -1
  24. package/dist/runtime/knowledge/types.d.ts +125 -1
  25. package/dist/runtime/knowledge.d.ts +2 -2
  26. package/dist/runtime/knowledge.js +1 -1
  27. package/dist/runtime/knowledge.js.map +1 -1
  28. package/dist/runtime/plan/guidance.d.ts +1 -1
  29. package/dist/runtime/plan/guidance.js +88 -25
  30. package/dist/runtime/plan/guidance.js.map +1 -1
  31. package/dist/runtime/plan/workflow.d.ts +1 -0
  32. package/dist/runtime/plan/workflow.js +32 -1
  33. package/dist/runtime/plan/workflow.js.map +1 -1
  34. package/dist/runtime/plan.d.ts +13 -0
  35. package/dist/runtime/plan.js +132 -79
  36. package/dist/runtime/plan.js.map +1 -1
  37. package/dist/runtime/policy-provenance.d.ts +17 -1
  38. package/dist/runtime/policy-provenance.js +42 -12
  39. package/dist/runtime/policy-provenance.js.map +1 -1
  40. package/dist/runtime/protocol-init.js +12 -7
  41. package/dist/runtime/protocol-init.js.map +1 -1
  42. package/dist/runtime/repair-packet.js +86 -4
  43. package/dist/runtime/repair-packet.js.map +1 -1
  44. package/dist/runtime/skills.d.ts +9 -0
  45. package/dist/runtime/skills.js +13 -0
  46. package/dist/runtime/skills.js.map +1 -1
  47. package/dist/runtime/state-explain.js +39 -7
  48. package/dist/runtime/state-explain.js.map +1 -1
  49. package/dist/runtime/state.js +36 -2
  50. package/dist/runtime/state.js.map +1 -1
  51. package/dist/runtime/task-context.d.ts +122 -0
  52. package/dist/runtime/task-context.js +1322 -0
  53. package/dist/runtime/task-context.js.map +1 -0
  54. package/dist/runtime/task-signals.d.ts +1 -0
  55. package/dist/runtime/task-signals.js +1 -1
  56. package/dist/runtime/task-signals.js.map +1 -1
  57. package/dist/runtime/ui-restoration.d.ts +3 -3
  58. package/dist/runtime/ui-restoration.js +3 -3
  59. package/dist/runtime/ui-restoration.js.map +1 -1
  60. package/dist/runtime/ui-source.d.ts +6 -0
  61. package/dist/runtime/ui-source.js +66 -0
  62. package/dist/runtime/ui-source.js.map +1 -0
  63. package/dist/runtime/verify.js +65 -25
  64. package/dist/runtime/verify.js.map +1 -1
  65. package/dist/schemas/types.d.ts +75 -2
  66. package/dist/schemas/validation.js +42 -7
  67. package/dist/schemas/validation.js.map +1 -1
  68. package/docs/DIRECTION.md +13 -11
  69. 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: kindTypeMap[kind],
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: parseKnowledgeSourcePaths(input.source, "knowledge add --source"),
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: kindTypeMap[kind],
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: cards.map(stripSearchable)
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) && !isKnowledgeSourceFile(projectRoot, sourcePath)))].sort();
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
- return { cards };
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 : 8;
331
- const cards = discoverKnowledgeCards(projectRoot)
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 { query: query ?? "", cards };
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) && !isKnowledgeSourceFile(projectRoot, 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(`knowledge card reference is ambiguous: ${target}. Use --path with one of: ${idMatches.map((item) => item.path).join(", ")}.`);
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(`knowledge card reference is ambiguous: ${target}. Use --path with one of: ${pathSuffixMatches.map((item) => item.path).join(", ")}.`);
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
- path: sourcePath,
1187
- cardCount: sourceCards.length,
1188
- activeCardCount: sourceCards.filter((card) => card.status === "active").length,
1189
- cardIds: sourceCards.map((card) => card.id).sort(),
1190
- scopes: [...new Set(sourceCards.flatMap((card) => card.scope))].sort(),
1191
- tags: [...new Set(sourceCards.flatMap((card) => card.tags))].sort(),
1192
- coverage: [...new Set(sourceCards.flatMap((card) => card.coverage))].sort(),
1193
- prdAnchors: [...new Set(sourceCards.flatMap((card) => card.prdAnchors))].sort()
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: relatedAtomicCards.map((card) => card.id).sort()
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
  }