frontend-harness 0.7.10 → 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 (68) hide show
  1. package/README.md +17 -10
  2. package/dist/cli/index.js +368 -65
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/runtime/builtin-skills.js +1 -1
  5. package/dist/runtime/clean.js +1 -0
  6. package/dist/runtime/clean.js.map +1 -1
  7. package/dist/runtime/command-taxonomy.js +6 -1
  8. package/dist/runtime/command-taxonomy.js.map +1 -1
  9. package/dist/runtime/context.d.ts +33 -2
  10. package/dist/runtime/context.js +63 -8
  11. package/dist/runtime/context.js.map +1 -1
  12. package/dist/runtime/evidence.d.ts +27 -0
  13. package/dist/runtime/evidence.js +240 -12
  14. package/dist/runtime/evidence.js.map +1 -1
  15. package/dist/runtime/ingest.d.ts +38 -0
  16. package/dist/runtime/ingest.js +157 -0
  17. package/dist/runtime/ingest.js.map +1 -0
  18. package/dist/runtime/knowledge/constants.js +1 -0
  19. package/dist/runtime/knowledge/constants.js.map +1 -1
  20. package/dist/runtime/knowledge/core.d.ts +7 -1
  21. package/dist/runtime/knowledge/core.js +329 -24
  22. package/dist/runtime/knowledge/core.js.map +1 -1
  23. package/dist/runtime/knowledge/types.d.ts +70 -1
  24. package/dist/runtime/knowledge.d.ts +2 -2
  25. package/dist/runtime/knowledge.js +1 -1
  26. package/dist/runtime/knowledge.js.map +1 -1
  27. package/dist/runtime/plan/guidance.d.ts +1 -1
  28. package/dist/runtime/plan/guidance.js +88 -25
  29. package/dist/runtime/plan/guidance.js.map +1 -1
  30. package/dist/runtime/plan/workflow.d.ts +1 -0
  31. package/dist/runtime/plan/workflow.js +32 -1
  32. package/dist/runtime/plan/workflow.js.map +1 -1
  33. package/dist/runtime/plan.d.ts +13 -0
  34. package/dist/runtime/plan.js +132 -79
  35. package/dist/runtime/plan.js.map +1 -1
  36. package/dist/runtime/policy-provenance.d.ts +17 -1
  37. package/dist/runtime/policy-provenance.js +42 -12
  38. package/dist/runtime/policy-provenance.js.map +1 -1
  39. package/dist/runtime/protocol-init.js +12 -7
  40. package/dist/runtime/protocol-init.js.map +1 -1
  41. package/dist/runtime/repair-packet.js +86 -4
  42. package/dist/runtime/repair-packet.js.map +1 -1
  43. package/dist/runtime/skills.d.ts +9 -0
  44. package/dist/runtime/skills.js +13 -0
  45. package/dist/runtime/skills.js.map +1 -1
  46. package/dist/runtime/state-explain.js +39 -7
  47. package/dist/runtime/state-explain.js.map +1 -1
  48. package/dist/runtime/state.js +36 -2
  49. package/dist/runtime/state.js.map +1 -1
  50. package/dist/runtime/task-context.d.ts +122 -0
  51. package/dist/runtime/task-context.js +1322 -0
  52. package/dist/runtime/task-context.js.map +1 -0
  53. package/dist/runtime/task-signals.d.ts +1 -0
  54. package/dist/runtime/task-signals.js +1 -1
  55. package/dist/runtime/task-signals.js.map +1 -1
  56. package/dist/runtime/ui-restoration.d.ts +3 -3
  57. package/dist/runtime/ui-restoration.js +3 -3
  58. package/dist/runtime/ui-restoration.js.map +1 -1
  59. package/dist/runtime/ui-source.d.ts +6 -0
  60. package/dist/runtime/ui-source.js +66 -0
  61. package/dist/runtime/ui-source.js.map +1 -0
  62. package/dist/runtime/verify.js +65 -25
  63. package/dist/runtime/verify.js.map +1 -1
  64. package/dist/schemas/types.d.ts +75 -2
  65. package/dist/schemas/validation.js +42 -7
  66. package/dist/schemas/validation.js.map +1 -1
  67. package/docs/DIRECTION.md +13 -11
  68. package/package.json +1 -1
@@ -3,7 +3,14 @@ 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_INDEX_CARDS = 20;
12
+ export const MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS = 20;
13
+ const MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS = 20;
7
14
  export function promoteKnowledge(projectRoot, input) {
8
15
  const title = input.title?.trim();
9
16
  const body = input.body?.trim();
@@ -121,11 +128,13 @@ export function addKnowledge(projectRoot, input) {
121
128
  const contentFields = atomicContentFields(kind, input);
122
129
  const inputSummary = input.summary?.trim();
123
130
  const contentSummary = inputSummary || summarizeAtomicContent(kind, subject, contentFields);
131
+ const sourcePaths = parseKnowledgeSourcePaths(input.source, "knowledge add --source");
132
+ const type = typeForAtomicKnowledge(kind, sourcePaths);
124
133
  const rendered = renderAtomicKnowledge({
125
134
  id,
126
135
  title,
127
136
  summary: contentSummary,
128
- type: kindTypeMap[kind],
137
+ type,
129
138
  kind,
130
139
  subject,
131
140
  status,
@@ -133,7 +142,7 @@ export function addKnowledge(projectRoot, input) {
133
142
  createdAt,
134
143
  scope: splitList(input.scope),
135
144
  tags: splitList(input.tags),
136
- sourcePaths: parseKnowledgeSourcePaths(input.source, "knowledge add --source"),
145
+ sourcePaths,
137
146
  verification: splitList(input.verification),
138
147
  coverage: splitList(input.coverage),
139
148
  prdAnchors: splitList(input.anchor),
@@ -144,12 +153,184 @@ export function addKnowledge(projectRoot, input) {
144
153
  path: relativePath,
145
154
  id,
146
155
  title,
147
- type: kindTypeMap[kind],
156
+ type,
148
157
  status,
149
158
  stability,
150
159
  createdAt
151
160
  };
152
161
  }
162
+ export function draftKnowledgeFromIngest(projectRoot, input) {
163
+ if (!input.ref?.trim()) {
164
+ throw new Error("knowledge draft requires --ref ingest://<kind>/<source-id>");
165
+ }
166
+ const record = showIngestRecord(projectRoot, input.ref);
167
+ const recommendedKind = recommendedKnowledgeKind(record.kind);
168
+ const recommendedType = typeForAtomicKnowledge(recommendedKind, [record.ingestRef]);
169
+ const subject = draftSubject(record);
170
+ const id = slugify(`${subject}-${recommendedKind}`) || `${record.id}-${recommendedKind}`;
171
+ const defaults = {
172
+ id,
173
+ kind: recommendedKind,
174
+ subject,
175
+ summary: record.summary,
176
+ source: record.ingestRef,
177
+ coverage: draftCoveragePlaceholder(record.kind),
178
+ verification: draftVerificationPlaceholder(record.kind),
179
+ scope: draftScope(record),
180
+ tags: draftTags(record.kind)
181
+ };
182
+ const contentFlag = contentFlagForKind(recommendedKind);
183
+ const updateCandidateSelection = draftUpdateCandidates(projectRoot, record, defaults);
184
+ const updateCandidates = updateCandidateSelection.candidates;
185
+ const args = [
186
+ "knowledge",
187
+ "add",
188
+ "--json",
189
+ "--id",
190
+ defaults.id,
191
+ "--kind",
192
+ defaults.kind,
193
+ "--subject",
194
+ defaults.subject,
195
+ "--summary",
196
+ defaults.summary,
197
+ `--${contentFlag}`,
198
+ `<atomic ${contentFlag} distilled from ${record.ingestRef}>`,
199
+ "--source",
200
+ defaults.source,
201
+ "--coverage",
202
+ defaults.coverage,
203
+ "--verification",
204
+ defaults.verification,
205
+ "--scope",
206
+ defaults.scope,
207
+ "--tags",
208
+ defaults.tags
209
+ ];
210
+ return {
211
+ contractVersion: 1,
212
+ ingestRef: record.ingestRef,
213
+ ingestKind: record.kind,
214
+ source: record.source.value,
215
+ summary: record.summary,
216
+ recommendedKind,
217
+ recommendedType,
218
+ recommendedAction: updateCandidates.length > 0 ? "update" : "add",
219
+ command: "frontend-harness knowledge add --json",
220
+ args,
221
+ updateCandidates,
222
+ limits: {
223
+ updateCandidates: updateCandidateSelection.limits
224
+ },
225
+ requiredEdits: [
226
+ ...(updateCandidateSelection.limits.total > 0
227
+ ? ["Prefer a matching update candidate before adding a new card for the same source."]
228
+ : []),
229
+ `Replace --${contentFlag} with one atomic durable fact; create separate cards for separate facts.`,
230
+ "Replace --coverage with the precise source section, endpoint, screen, or acceptance block.",
231
+ "Replace --verification with the concrete check or evidence that proves the fact still holds.",
232
+ "Narrow --scope and --tags so future context selection can find the card without broad matching."
233
+ ],
234
+ defaults
235
+ };
236
+ }
237
+ function draftUpdateCandidates(projectRoot, record, defaults) {
238
+ const producedIds = new Set(record.producedKnowledgeCardIds);
239
+ const cards = discoverKnowledgeCards(projectRoot);
240
+ const matches = cards
241
+ .filter((card) => producedIds.has(card.id) || card.sourcePaths.some((sourcePath) => sameKnowledgeSourceReference(sourcePath, record.ingestRef) || sameKnowledgeSourceReference(sourcePath, record.source.value)))
242
+ .sort((left, right) => left.id.localeCompare(right.id));
243
+ const selected = matches
244
+ .slice(0, MAX_KNOWLEDGE_DRAFT_UPDATE_CANDIDATES)
245
+ .map((card) => ({
246
+ id: card.id,
247
+ path: card.path,
248
+ title: card.title,
249
+ summary: card.summary,
250
+ sourcePaths: card.sourcePaths,
251
+ command: "frontend-harness knowledge update --json",
252
+ args: [
253
+ "knowledge",
254
+ "update",
255
+ "--json",
256
+ "--id",
257
+ card.id,
258
+ "--summary",
259
+ defaults.summary,
260
+ "--source",
261
+ defaults.source,
262
+ "--coverage",
263
+ defaults.coverage,
264
+ "--verification",
265
+ defaults.verification,
266
+ "--scope",
267
+ defaults.scope,
268
+ "--tags",
269
+ defaults.tags
270
+ ]
271
+ }));
272
+ return {
273
+ candidates: selected,
274
+ limits: {
275
+ selected: selected.length,
276
+ total: matches.length,
277
+ omitted: Math.max(0, matches.length - selected.length)
278
+ }
279
+ };
280
+ }
281
+ function recommendedKnowledgeKind(kind) {
282
+ if (kind === "openapi") {
283
+ return "workflow";
284
+ }
285
+ return "rule";
286
+ }
287
+ function typeForAtomicKnowledge(kind, sourcePaths) {
288
+ if (sourcePaths.some(isApiContractSourcePath)) {
289
+ return "api_contract";
290
+ }
291
+ return kindTypeMap[kind];
292
+ }
293
+ function isApiContractSourcePath(sourcePath) {
294
+ const normalized = sourcePath.replace(/\\/g, "/");
295
+ if (/^ingest:\/\/openapi\//i.test(normalized)) {
296
+ return isIngestReference(normalized);
297
+ }
298
+ if (/(^|\/)(openapi|swagger)(\/|[-_.])/i.test(normalized)) {
299
+ return true;
300
+ }
301
+ return /(^|\/)api\/[^/]+\.(?:json|ya?ml)$/i.test(normalized);
302
+ }
303
+ function draftSubject(record) {
304
+ const sourceName = path.basename(record.source.value).replace(/\.[^.]+$/, "");
305
+ const raw = sourceName || record.id;
306
+ return titleCase(raw.replace(/[-_.]+/g, " ")).trim() || record.id;
307
+ }
308
+ function draftScope(record) {
309
+ return record.id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
310
+ }
311
+ function draftCoveragePlaceholder(kind) {
312
+ if (kind === "ui-package") {
313
+ return "<screen, component, state, or visual reference>";
314
+ }
315
+ if (kind === "openapi") {
316
+ return "<endpoint, schema, operation, or contract behavior>";
317
+ }
318
+ return "<precise section, endpoint, screen, or acceptance block>";
319
+ }
320
+ function draftVerificationPlaceholder(kind) {
321
+ if (kind === "ui-package") {
322
+ return "<visual, responsive, or interaction evidence required>";
323
+ }
324
+ if (kind === "openapi") {
325
+ return "<contract shape check or mocked integration evidence required>";
326
+ }
327
+ return "<specific check or evidence required>";
328
+ }
329
+ function draftTags(kind) {
330
+ return kind === "ui-package"
331
+ ? "ui-package,ui"
332
+ : kind.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
333
+ }
153
334
  export function updateKnowledge(projectRoot, input) {
154
335
  const target = input.id?.trim() || input.path?.trim();
155
336
  if (!target) {
@@ -231,6 +412,7 @@ export function indexKnowledge(projectRoot) {
231
412
  const cards = discoverKnowledgeCards(projectRoot);
232
413
  const prdSources = summarizePrdSources(projectRoot, cards);
233
414
  const moduleSummaries = summarizeModules(cards);
415
+ const selectedCards = cards.slice(0, MAX_KNOWLEDGE_INDEX_CARDS);
234
416
  const result = {
235
417
  artifactPath: relativeHarnessPath("knowledge", "index.json"),
236
418
  cardCount: cards.length,
@@ -242,7 +424,14 @@ export function indexKnowledge(projectRoot) {
242
424
  byKind: countByKind(cards),
243
425
  moduleSummaries,
244
426
  prdSources,
245
- cards: cards.map(stripSearchable)
427
+ cards: selectedCards.map(stripSearchable),
428
+ limits: {
429
+ cards: {
430
+ selected: selectedCards.length,
431
+ total: cards.length,
432
+ omitted: Math.max(0, cards.length - selectedCards.length)
433
+ }
434
+ }
246
435
  };
247
436
  writeJson(path.join(projectRoot, result.artifactPath), result);
248
437
  return result;
@@ -270,7 +459,7 @@ export function checkKnowledgeCoverage(projectRoot, options = {}) {
270
459
  .filter((source) => source.activeCardCount === 0)
271
460
  .map((source) => source.path);
272
461
  const missingSourcePaths = [...new Set(cards.flatMap((card) => card.sourcePaths)
273
- .filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !isKnowledgeSourceFile(projectRoot, sourcePath)))].sort();
462
+ .filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)))].sort();
274
463
  const cardsWithoutSource = cards
275
464
  .filter((card) => card.status === "active" && card.type === "prd_semantics" && card.sourcePaths.length === 0)
276
465
  .map((card) => card.path);
@@ -320,15 +509,23 @@ export function listKnowledge(projectRoot, options = {}) {
320
509
  const cards = discoverKnowledgeCards(projectRoot)
321
510
  .filter((card) => options.includeDeprecated || card.status === "active")
322
511
  .map(stripSearchable);
323
- return { cards };
512
+ const selected = cards.slice(0, MAX_KNOWLEDGE_LIST_CARDS);
513
+ return {
514
+ cards: selected,
515
+ limits: {
516
+ selected: selected.length,
517
+ total: cards.length,
518
+ omitted: Math.max(0, cards.length - selected.length)
519
+ }
520
+ };
324
521
  }
325
522
  export function searchKnowledge(projectRoot, query, options = {}) {
326
523
  const normalizedQuery = normalize(query ?? "");
327
524
  if (!normalizedQuery) {
328
525
  throw new Error("knowledge search requires --query");
329
526
  }
330
- const limit = options.limit && options.limit > 0 ? options.limit : 8;
331
- const cards = discoverKnowledgeCards(projectRoot)
527
+ const limit = options.limit && options.limit > 0 ? options.limit : MAX_KNOWLEDGE_SEARCH_RESULTS;
528
+ const matches = discoverKnowledgeCards(projectRoot)
332
529
  .filter((card) => options.includeDeprecated || card.status === "active")
333
530
  .map((card) => ({
334
531
  card,
@@ -340,13 +537,22 @@ export function searchKnowledge(projectRoot, query, options = {}) {
340
537
  return right.score - left.score;
341
538
  }
342
539
  return left.card.path.localeCompare(right.card.path);
343
- })
540
+ });
541
+ const cards = matches
344
542
  .slice(0, limit)
345
543
  .map(({ card, score }) => ({
346
544
  ...stripSearchable(card),
347
545
  score
348
546
  }));
349
- return { query: query ?? "", cards };
547
+ return {
548
+ query: query ?? "",
549
+ cards,
550
+ limits: {
551
+ selected: cards.length,
552
+ total: matches.length,
553
+ omitted: Math.max(0, matches.length - cards.length)
554
+ }
555
+ };
350
556
  }
351
557
  export function showKnowledge(projectRoot, idOrPath) {
352
558
  const target = idOrPath?.trim();
@@ -368,6 +574,9 @@ export function showKnowledge(projectRoot, idOrPath) {
368
574
  body: extractMarkdownBody(content)
369
575
  };
370
576
  }
577
+ function sameKnowledgeSourceReference(left, right) {
578
+ return left.replace(/\\/g, "/").toLowerCase() === right.replace(/\\/g, "/").toLowerCase();
579
+ }
371
580
  export function checkKnowledge(projectRoot) {
372
581
  const knowledgeRoot = harnessPath(projectRoot, "knowledge");
373
582
  const relativeRoot = path.relative(projectRoot, knowledgeRoot).split(path.sep).join(path.posix.sep);
@@ -431,6 +640,15 @@ export function checkKnowledge(projectRoot) {
431
640
  if (contentValue && normalize(contentValue) === normalize(parsed.metadata.summary ?? "")) {
432
641
  warnings.push(`${relativePath} summary duplicates the primary atomic content; use summary for context and the primary field for the durable rule.`);
433
642
  }
643
+ if (parsed.metadata.status === "active" && parsed.metadata.sourcePaths.some((sourcePath) => sourcePath.toLowerCase().startsWith("ingest://"))) {
644
+ errors.push(...ingestLinkedAtomicMetadataErrors(relativePath, parsed.metadata));
645
+ }
646
+ for (const coverage of parsed.metadata.coverage.filter(isDraftPlaceholderCoverage)) {
647
+ errors.push(`${relativePath} coverage contains an unedited knowledge draft placeholder: ${coverage}.`);
648
+ }
649
+ for (const verification of parsed.metadata.verification.filter(isDraftPlaceholderVerification)) {
650
+ errors.push(`${relativePath} verification contains an unedited knowledge draft placeholder: ${verification}.`);
651
+ }
434
652
  if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.length === 0) {
435
653
  warnings.push(`${relativePath} should include coverage items for PRD block traceability.`);
436
654
  }
@@ -442,7 +660,7 @@ export function checkKnowledge(projectRoot) {
442
660
  warnings.push(`${relativePath} frontmatter coverage disagrees with body traceability.`);
443
661
  }
444
662
  for (const sourcePath of parsed.metadata.sourcePaths) {
445
- if (sourcePath && !isExternalReference(sourcePath) && !isKnowledgeSourceFile(projectRoot, sourcePath)) {
663
+ if (sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)) {
446
664
  warnings.push(`${relativePath} source path is missing or unsafe: ${sourcePath}.`);
447
665
  }
448
666
  }
@@ -546,17 +764,22 @@ function resolveKnowledgeCardReference(cards, target) {
546
764
  return idMatches[0] ?? null;
547
765
  }
548
766
  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(", ")}.`);
767
+ throw new Error(formatAmbiguousKnowledgeReference(target, idMatches));
550
768
  }
551
769
  const pathSuffixMatches = cards.filter((item) => item.path.endsWith(`/${target}`));
552
770
  if (pathSuffixMatches.length === 1) {
553
771
  return pathSuffixMatches[0] ?? null;
554
772
  }
555
773
  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(", ")}.`);
774
+ throw new Error(formatAmbiguousKnowledgeReference(target, pathSuffixMatches));
557
775
  }
558
776
  return null;
559
777
  }
778
+ function formatAmbiguousKnowledgeReference(target, matches) {
779
+ const selected = matches.slice(0, MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS).map((item) => item.path);
780
+ const omitted = Math.max(0, matches.length - selected.length);
781
+ return `knowledge card reference is ambiguous: ${target}. Use --path with one of (selected: ${selected.length}; total: ${matches.length}; omitted: ${omitted}): ${selected.join(", ")}.`;
782
+ }
560
783
  function parseKnowledgeFrontmatter(content) {
561
784
  const metadata = {
562
785
  scope: [],
@@ -932,6 +1155,9 @@ function validateKnowledgeSourcePath(value) {
932
1155
  if (!value.trim()) {
933
1156
  return "empty";
934
1157
  }
1158
+ if (value.toLowerCase().startsWith("ingest://")) {
1159
+ return isIngestReference(value) ? null : "unsafe";
1160
+ }
935
1161
  if (isExternalReference(value)) {
936
1162
  return null;
937
1163
  }
@@ -946,6 +1172,13 @@ function validateKnowledgeSourcePath(value) {
946
1172
  function isKnowledgeSourceFile(projectRoot, sourcePath) {
947
1173
  return isRegularFileWithoutSymlink(path.join(projectRoot, sourcePath));
948
1174
  }
1175
+ function isResolvedKnowledgeSource(projectRoot, sourcePath) {
1176
+ if (isIngestReference(sourcePath)) {
1177
+ const parsed = parseIngestReference(sourcePath);
1178
+ return parsed !== null && isRegularFileWithoutSymlink(harnessPath(projectRoot, "ingest", parsed.kind, `${parsed.id}.json`));
1179
+ }
1180
+ return isKnowledgeSourceFile(projectRoot, sourcePath);
1181
+ }
949
1182
  function mergeList(existing, next) {
950
1183
  if (next === undefined) {
951
1184
  return existing;
@@ -1142,11 +1375,29 @@ function extractMarkdownBody(content) {
1142
1375
  return content.slice(endIndex).replace(/^\r?\n---\r?\n?/, "").trim();
1143
1376
  }
1144
1377
  function isExternalReference(value) {
1378
+ if (value.toLowerCase().startsWith("ingest://")) {
1379
+ return false;
1380
+ }
1145
1381
  if (/^[A-Za-z]:/.test(value)) {
1146
1382
  return false;
1147
1383
  }
1148
1384
  return /^[a-z][a-z0-9+.-]*:/i.test(value);
1149
1385
  }
1386
+ function isIngestReference(value) {
1387
+ return /^ingest:\/\/(?:prd|openapi|ui-package)\/[a-z0-9][a-z0-9._-]{1,80}$/i.test(value)
1388
+ && !value.includes("..")
1389
+ && !/[?#]/.test(value);
1390
+ }
1391
+ function parseIngestReference(value) {
1392
+ const match = /^ingest:\/\/(prd|openapi|ui-package)\/([a-z0-9][a-z0-9._-]{1,80})$/i.exec(value);
1393
+ if (!match?.[1] || !match[2]) {
1394
+ return null;
1395
+ }
1396
+ return {
1397
+ kind: match[1].toLowerCase(),
1398
+ id: match[2]
1399
+ };
1400
+ }
1150
1401
  function isBroadCoverageItem(value) {
1151
1402
  const normalized = value.trim().toLowerCase();
1152
1403
  if (!normalized) {
@@ -1167,6 +1418,35 @@ function hasCoverageBodyDrift(projectRoot, card) {
1167
1418
  const content = safeRead(path.join(projectRoot, card.path));
1168
1419
  return content !== null && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content));
1169
1420
  }
1421
+ function isDraftPlaceholderCoverage(value) {
1422
+ const normalized = value.trim().toLowerCase();
1423
+ return (normalized.startsWith("<") || normalized.endsWith(">"))
1424
+ && (normalized.includes("precise section")
1425
+ || normalized.includes("acceptance block")
1426
+ || normalized === "<precise"
1427
+ || normalized === "or acceptance block>");
1428
+ }
1429
+ function isDraftPlaceholderVerification(value) {
1430
+ const normalized = value.trim().toLowerCase();
1431
+ return /^<.*>$/.test(normalized)
1432
+ && normalized.includes("specific")
1433
+ && (normalized.includes("check") || normalized.includes("evidence"));
1434
+ }
1435
+ function ingestLinkedAtomicMetadataErrors(relativePath, metadata) {
1436
+ const errors = [];
1437
+ const requiredLists = [
1438
+ ["scope", metadata.scope],
1439
+ ["tags", metadata.tags],
1440
+ ["coverage", metadata.coverage],
1441
+ ["verification", metadata.verification]
1442
+ ];
1443
+ for (const [field, values] of requiredLists) {
1444
+ if (values.length === 0) {
1445
+ errors.push(`${relativePath} active ingest-linked atomic knowledge must include ${field}.`);
1446
+ }
1447
+ }
1448
+ return errors;
1449
+ }
1170
1450
  function summarizePrdSources(projectRoot, cards, requiredSources = []) {
1171
1451
  const grouped = new Map();
1172
1452
  for (const sourcePath of discoverPrdSourceFiles(projectRoot)) {
@@ -1182,16 +1462,30 @@ function summarizePrdSources(projectRoot, cards, requiredSources = []) {
1182
1462
  }
1183
1463
  return [...grouped.entries()]
1184
1464
  .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
- }));
1465
+ .map(([sourcePath, sourceCards]) => {
1466
+ const cardIds = sourceCards.map((card) => card.id).sort();
1467
+ const scopes = [...new Set(sourceCards.flatMap((card) => card.scope))].sort();
1468
+ const tags = [...new Set(sourceCards.flatMap((card) => card.tags))].sort();
1469
+ const coverage = [...new Set(sourceCards.flatMap((card) => card.coverage))].sort();
1470
+ const prdAnchors = [...new Set(sourceCards.flatMap((card) => card.prdAnchors))].sort();
1471
+ return {
1472
+ path: sourcePath,
1473
+ cardCount: sourceCards.length,
1474
+ activeCardCount: sourceCards.filter((card) => card.status === "active").length,
1475
+ cardIds: selectedIndexDetails(cardIds),
1476
+ scopes: selectedIndexDetails(scopes),
1477
+ tags: selectedIndexDetails(tags),
1478
+ coverage: selectedIndexDetails(coverage),
1479
+ prdAnchors: selectedIndexDetails(prdAnchors),
1480
+ omitted: {
1481
+ cardIds: omittedIndexDetails(cardIds),
1482
+ scopes: omittedIndexDetails(scopes),
1483
+ tags: omittedIndexDetails(tags),
1484
+ coverage: omittedIndexDetails(coverage),
1485
+ prdAnchors: omittedIndexDetails(prdAnchors)
1486
+ }
1487
+ };
1488
+ });
1195
1489
  }
1196
1490
  function summarizeModules(cards) {
1197
1491
  const atomicCards = cards.filter((card) => card.kind && card.status === "active");
@@ -1204,6 +1498,7 @@ function summarizeModules(cards) {
1204
1498
  card.related.includes(moduleCard.id) ||
1205
1499
  card.sourcePaths.some((sourcePath) => moduleSources.has(sourcePath)) ||
1206
1500
  card.scope.some((scope) => moduleCard.scope.includes(scope)));
1501
+ const atomicCardIds = relatedAtomicCards.map((card) => card.id).sort();
1207
1502
  return {
1208
1503
  id: moduleCard.id,
1209
1504
  path: moduleCard.path,
@@ -1214,11 +1509,18 @@ function summarizeModules(cards) {
1214
1509
  tags: moduleCard.tags,
1215
1510
  related: moduleCard.related,
1216
1511
  atomicCardCount: relatedAtomicCards.length,
1217
- atomicCardIds: relatedAtomicCards.map((card) => card.id).sort()
1512
+ atomicCardIds: selectedIndexDetails(atomicCardIds),
1513
+ omittedAtomicCardIds: omittedIndexDetails(atomicCardIds)
1218
1514
  };
1219
1515
  })
1220
1516
  .sort((left, right) => left.path.localeCompare(right.path));
1221
1517
  }
1518
+ function selectedIndexDetails(values) {
1519
+ return values.slice(0, MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS);
1520
+ }
1521
+ function omittedIndexDetails(values) {
1522
+ return Math.max(0, values.length - MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS);
1523
+ }
1222
1524
  function discoverPrdSourceFiles(projectRoot) {
1223
1525
  const roots = ["docs/prd", "docs/requirements", "prd", "requirements"];
1224
1526
  const rootSources = roots.flatMap((root) => {
@@ -1237,6 +1539,9 @@ function discoverPrdSourceFiles(projectRoot) {
1237
1539
  }
1238
1540
  function isPrdSourcePath(sourcePath) {
1239
1541
  const normalized = sourcePath.replace(/\\/g, "/");
1542
+ if (/^ingest:\/\/prd\//i.test(normalized)) {
1543
+ return isIngestReference(normalized);
1544
+ }
1240
1545
  if (/(^|\/)(prd|requirements?)(\/|[-_.])|(^|\/)docs\/(prd|requirements?)(\/|$)/i.test(normalized)) {
1241
1546
  return true;
1242
1547
  }