frontend-harness 0.7.9 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +20 -10
  3. package/dist/cli/index.js +388 -66
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/runtime/builtin-skills.js +1 -1
  6. package/dist/runtime/clean.js +25 -3
  7. package/dist/runtime/clean.js.map +1 -1
  8. package/dist/runtime/command-taxonomy.js +6 -1
  9. package/dist/runtime/command-taxonomy.js.map +1 -1
  10. package/dist/runtime/context.d.ts +33 -2
  11. package/dist/runtime/context.js +112 -13
  12. package/dist/runtime/context.js.map +1 -1
  13. package/dist/runtime/evidence.d.ts +27 -0
  14. package/dist/runtime/evidence.js +302 -19
  15. package/dist/runtime/evidence.js.map +1 -1
  16. package/dist/runtime/graph.js +11 -4
  17. package/dist/runtime/graph.js.map +1 -1
  18. package/dist/runtime/ingest.d.ts +38 -0
  19. package/dist/runtime/ingest.js +157 -0
  20. package/dist/runtime/ingest.js.map +1 -0
  21. package/dist/runtime/knowledge/constants.d.ts +9 -0
  22. package/dist/runtime/knowledge/constants.js +25 -0
  23. package/dist/runtime/knowledge/constants.js.map +1 -0
  24. package/dist/runtime/knowledge/core.d.ts +25 -0
  25. package/dist/runtime/knowledge/core.js +1569 -0
  26. package/dist/runtime/knowledge/core.js.map +1 -0
  27. package/dist/runtime/knowledge/types.d.ts +241 -0
  28. package/dist/runtime/knowledge/types.js +2 -0
  29. package/dist/runtime/knowledge/types.js.map +1 -0
  30. package/dist/runtime/knowledge.d.ts +2 -188
  31. package/dist/runtime/knowledge.js +1 -1126
  32. package/dist/runtime/knowledge.js.map +1 -1
  33. package/dist/runtime/plan/component-resolver.js +17 -6
  34. package/dist/runtime/plan/component-resolver.js.map +1 -1
  35. package/dist/runtime/plan/guidance.d.ts +1 -1
  36. package/dist/runtime/plan/guidance.js +88 -25
  37. package/dist/runtime/plan/guidance.js.map +1 -1
  38. package/dist/runtime/plan/proposal.js +9 -0
  39. package/dist/runtime/plan/proposal.js.map +1 -1
  40. package/dist/runtime/plan/workflow.d.ts +1 -0
  41. package/dist/runtime/plan/workflow.js +32 -1
  42. package/dist/runtime/plan/workflow.js.map +1 -1
  43. package/dist/runtime/plan.d.ts +13 -0
  44. package/dist/runtime/plan.js +137 -82
  45. package/dist/runtime/plan.js.map +1 -1
  46. package/dist/runtime/policy-provenance.d.ts +17 -1
  47. package/dist/runtime/policy-provenance.js +84 -16
  48. package/dist/runtime/policy-provenance.js.map +1 -1
  49. package/dist/runtime/project-discovery.js +32 -17
  50. package/dist/runtime/project-discovery.js.map +1 -1
  51. package/dist/runtime/project-paths.js +7 -1
  52. package/dist/runtime/project-paths.js.map +1 -1
  53. package/dist/runtime/protocol-init.js +117 -26
  54. package/dist/runtime/protocol-init.js.map +1 -1
  55. package/dist/runtime/repair-decision.js +76 -16
  56. package/dist/runtime/repair-decision.js.map +1 -1
  57. package/dist/runtime/repair-packet.js +141 -20
  58. package/dist/runtime/repair-packet.js.map +1 -1
  59. package/dist/runtime/scaffold/vue-template.js +1 -1
  60. package/dist/runtime/scaffold/vue-template.js.map +1 -1
  61. package/dist/runtime/scaffold.js +44 -8
  62. package/dist/runtime/scaffold.js.map +1 -1
  63. package/dist/runtime/skills.d.ts +9 -0
  64. package/dist/runtime/skills.js +133 -18
  65. package/dist/runtime/skills.js.map +1 -1
  66. package/dist/runtime/state-explain.js +39 -7
  67. package/dist/runtime/state-explain.js.map +1 -1
  68. package/dist/runtime/state.js +85 -10
  69. package/dist/runtime/state.js.map +1 -1
  70. package/dist/runtime/task-context.d.ts +122 -0
  71. package/dist/runtime/task-context.js +1322 -0
  72. package/dist/runtime/task-context.js.map +1 -0
  73. package/dist/runtime/task-signals.d.ts +1 -0
  74. package/dist/runtime/task-signals.js +1 -1
  75. package/dist/runtime/task-signals.js.map +1 -1
  76. package/dist/runtime/ui-restoration.d.ts +3 -3
  77. package/dist/runtime/ui-restoration.js +3 -3
  78. package/dist/runtime/ui-restoration.js.map +1 -1
  79. package/dist/runtime/ui-source.d.ts +6 -0
  80. package/dist/runtime/ui-source.js +66 -0
  81. package/dist/runtime/ui-source.js.map +1 -0
  82. package/dist/runtime/units.js +11 -4
  83. package/dist/runtime/units.js.map +1 -1
  84. package/dist/runtime/verification-commands.js +95 -21
  85. package/dist/runtime/verification-commands.js.map +1 -1
  86. package/dist/runtime/verify.js +129 -43
  87. package/dist/runtime/verify.js.map +1 -1
  88. package/dist/runtime/visual-compare.js +104 -12
  89. package/dist/runtime/visual-compare.js.map +1 -1
  90. package/dist/schemas/types.d.ts +75 -2
  91. package/dist/schemas/validation.js +257 -6
  92. package/dist/schemas/validation.js.map +1 -1
  93. package/dist/storage/json.d.ts +9 -1
  94. package/dist/storage/json.js +106 -8
  95. package/dist/storage/json.js.map +1 -1
  96. package/docs/DIRECTION.md +13 -11
  97. package/docs/RELEASE.md +5 -1
  98. package/package.json +1 -1
@@ -0,0 +1,1569 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isDirectoryWithoutSymlink, isRegularFileWithoutSymlink, writeJson, writeText } from "../../storage/json.js";
4
+ import { harnessPath, relativeHarnessPath } from "../../storage/paths.js";
5
+ import { normalizeProjectRelativePath } from "../project-paths.js";
6
+ import { showIngestRecord } from "../ingest.js";
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;
14
+ export function promoteKnowledge(projectRoot, input) {
15
+ const title = input.title?.trim();
16
+ const body = input.body?.trim();
17
+ if (!title) {
18
+ throw new Error("knowledge promote requires --title");
19
+ }
20
+ if (!body) {
21
+ throw new Error("knowledge promote requires --body");
22
+ }
23
+ const type = parseKnowledgeType(input.type ?? "prd_semantics", "knowledge promote --type");
24
+ const status = parseKnowledgeStatus(input.status ?? "active", "knowledge promote --status");
25
+ const stability = parseKnowledgeStability(input.stability ?? "stable", "knowledge promote --stability");
26
+ const createdAt = new Date().toISOString();
27
+ const id = slugify(title) || `knowledge-${formatKnowledgeStamp(createdAt).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`;
28
+ const fileName = `${formatKnowledgeStamp(createdAt)}-${id}.md`;
29
+ const relativePath = relativeHarnessPath("knowledge", fileName);
30
+ const content = renderKnowledge({
31
+ id,
32
+ title,
33
+ body,
34
+ type,
35
+ status,
36
+ stability,
37
+ createdAt,
38
+ scope: splitList(input.scope),
39
+ tags: splitList(input.tags),
40
+ sourcePaths: parseKnowledgeSourcePaths(input.source, "knowledge promote --source")
41
+ });
42
+ writeText(path.join(projectRoot, relativePath), content);
43
+ return {
44
+ path: relativePath,
45
+ id,
46
+ title,
47
+ type,
48
+ status,
49
+ stability,
50
+ createdAt
51
+ };
52
+ }
53
+ export function createModuleKnowledge(projectRoot, input) {
54
+ const title = input.title?.trim();
55
+ const summary = input.summary?.trim();
56
+ const body = input.body?.trim();
57
+ if (!title) {
58
+ throw new Error("knowledge module requires --title");
59
+ }
60
+ if (!summary) {
61
+ throw new Error("knowledge module requires --summary");
62
+ }
63
+ if (!body) {
64
+ throw new Error("knowledge module requires --body");
65
+ }
66
+ const status = parseKnowledgeStatus(input.status ?? "active", "knowledge module --status");
67
+ const stability = parseKnowledgeStability(input.stability ?? "stable", "knowledge module --stability");
68
+ const createdAt = new Date().toISOString();
69
+ const id = input.id?.trim() || slugify(title);
70
+ if (!id || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(id)) {
71
+ throw new Error("knowledge module --id must be kebab-case when provided");
72
+ }
73
+ const relativePath = relativeHarnessPath("knowledge", "modules", `${id}.md`);
74
+ const fullPath = path.join(projectRoot, relativePath);
75
+ if (fs.existsSync(fullPath)) {
76
+ throw new Error(`knowledge module already exists: ${relativePath}`);
77
+ }
78
+ const content = renderKnowledgeFromMetadata({
79
+ id,
80
+ title,
81
+ summary,
82
+ type: "module_summary",
83
+ status,
84
+ stability,
85
+ updatedAt: createdAt,
86
+ scope: splitList(input.scope),
87
+ tags: splitList(input.tags),
88
+ verification: splitList(input.verification),
89
+ coverage: splitList(input.coverage),
90
+ prdAnchors: splitList(input.anchor),
91
+ sourcePaths: parseKnowledgeSourcePaths(input.source, "knowledge module --source"),
92
+ related: splitList(input.related)
93
+ }, body);
94
+ writeText(fullPath, content);
95
+ return {
96
+ path: relativePath,
97
+ id,
98
+ title,
99
+ type: "module_summary",
100
+ status,
101
+ stability,
102
+ createdAt
103
+ };
104
+ }
105
+ export function addKnowledge(projectRoot, input) {
106
+ const kind = parseKnowledgeKind(input.kind ?? "", "knowledge add --kind");
107
+ const subject = input.subject?.trim();
108
+ if (!subject) {
109
+ throw new Error("knowledge add requires --subject");
110
+ }
111
+ const content = atomicContent(kind, input);
112
+ if (!content) {
113
+ throw new Error(`knowledge add requires --${contentFlagForKind(kind)} or --note`);
114
+ }
115
+ const status = parseKnowledgeStatus(input.status ?? "active", "knowledge add --status");
116
+ const stability = parseKnowledgeStability(input.stability ?? "stable", "knowledge add --stability");
117
+ const createdAt = new Date().toISOString();
118
+ const id = input.id?.trim() || slugify(`${subject}-${kind}-${content}`);
119
+ if (!id || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(id)) {
120
+ throw new Error("knowledge add --id must be kebab-case when provided");
121
+ }
122
+ const relativePath = relativeHarnessPath("knowledge", kind, `${id}.md`);
123
+ const fullPath = path.join(projectRoot, relativePath);
124
+ if (fs.existsSync(fullPath)) {
125
+ throw new Error(`knowledge card already exists: ${relativePath}`);
126
+ }
127
+ const title = `${subject} ${titleCase(kind)}`;
128
+ const contentFields = atomicContentFields(kind, input);
129
+ const inputSummary = input.summary?.trim();
130
+ const contentSummary = inputSummary || summarizeAtomicContent(kind, subject, contentFields);
131
+ const sourcePaths = parseKnowledgeSourcePaths(input.source, "knowledge add --source");
132
+ const type = typeForAtomicKnowledge(kind, sourcePaths);
133
+ const rendered = renderAtomicKnowledge({
134
+ id,
135
+ title,
136
+ summary: contentSummary,
137
+ type,
138
+ kind,
139
+ subject,
140
+ status,
141
+ stability,
142
+ createdAt,
143
+ scope: splitList(input.scope),
144
+ tags: splitList(input.tags),
145
+ sourcePaths,
146
+ verification: splitList(input.verification),
147
+ coverage: splitList(input.coverage),
148
+ prdAnchors: splitList(input.anchor),
149
+ contentFields
150
+ });
151
+ writeText(fullPath, rendered);
152
+ return {
153
+ path: relativePath,
154
+ id,
155
+ title,
156
+ type,
157
+ status,
158
+ stability,
159
+ createdAt
160
+ };
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
+ }
334
+ export function updateKnowledge(projectRoot, input) {
335
+ const target = input.id?.trim() || input.path?.trim();
336
+ if (!target) {
337
+ throw new Error("knowledge update requires --id or --path");
338
+ }
339
+ const cards = discoverKnowledgeCards(projectRoot);
340
+ const card = resolveKnowledgeCardReference(cards, target);
341
+ if (!card) {
342
+ throw new Error(`knowledge card not found: ${target}`);
343
+ }
344
+ const fullPath = path.join(projectRoot, card.path);
345
+ const content = safeRead(fullPath);
346
+ if (content === null) {
347
+ throw new Error(`knowledge card cannot be read: ${card.path}`);
348
+ }
349
+ const parsed = parseKnowledgeFrontmatter(content);
350
+ if (!parsed.hasFrontmatter) {
351
+ throw new Error(`${card.path} must include YAML frontmatter.`);
352
+ }
353
+ const metadata = parsed.metadata;
354
+ if (!metadata.id || !metadata.title || !metadata.summary || !metadata.type || !metadata.status || !metadata.stability) {
355
+ throw new Error(`${card.path} has incomplete knowledge metadata.`);
356
+ }
357
+ const nextStatus = input.status ? parseKnowledgeStatus(input.status, "knowledge update --status") : metadata.status;
358
+ const nextStability = input.stability ? parseKnowledgeStability(input.stability, "knowledge update --stability") : metadata.stability;
359
+ const updatedAt = new Date().toISOString();
360
+ const nextMetadata = {
361
+ ...metadata,
362
+ id: metadata.id,
363
+ title: input.title?.trim() || metadata.title,
364
+ summary: input.summary?.trim() || metadata.summary,
365
+ type: metadata.type,
366
+ status: nextStatus,
367
+ stability: nextStability,
368
+ updatedAt,
369
+ scope: mergeList(metadata.scope, input.scope),
370
+ tags: mergeList(metadata.tags, input.tags),
371
+ verification: mergeList(metadata.verification, input.verification),
372
+ coverage: mergeList(metadata.coverage, input.coverage),
373
+ prdAnchors: mergeList(metadata.prdAnchors, input.anchor),
374
+ sourcePaths: mergeKnowledgeSourcePaths(metadata.sourcePaths, input.source, "knowledge update --source"),
375
+ related: mergeList(metadata.related, input.related)
376
+ };
377
+ for (const key of ["rule", "workflow", "permission", "term", "note"]) {
378
+ const value = input[key]?.trim();
379
+ if (value) {
380
+ nextMetadata[key] = value;
381
+ }
382
+ }
383
+ const body = input.body?.trim() || extractMarkdownBody(content);
384
+ const errors = validateKnowledgeMetadata(card.path, nextMetadata);
385
+ if (errors.length) {
386
+ throw new Error(errors.join(" "));
387
+ }
388
+ writeText(fullPath, renderKnowledgeFromMetadata(nextMetadata, body));
389
+ return {
390
+ path: card.path,
391
+ id: nextMetadata.id,
392
+ title: nextMetadata.title,
393
+ type: nextMetadata.type,
394
+ status: nextStatus,
395
+ stability: nextStability,
396
+ createdAt: updatedAt
397
+ };
398
+ }
399
+ export function discoverKnowledgeCards(projectRoot) {
400
+ const knowledgeRoot = harnessPath(projectRoot, "knowledge");
401
+ if (!isReadableDirectory(knowledgeRoot)) {
402
+ return [];
403
+ }
404
+ return listMarkdownFiles(knowledgeRoot)
405
+ .flatMap((fullPath) => {
406
+ const card = describeKnowledgeCard(projectRoot, fullPath);
407
+ return card ? [card] : [];
408
+ })
409
+ .sort((left, right) => left.path.localeCompare(right.path));
410
+ }
411
+ export function indexKnowledge(projectRoot) {
412
+ const cards = discoverKnowledgeCards(projectRoot);
413
+ const prdSources = summarizePrdSources(projectRoot, cards);
414
+ const moduleSummaries = summarizeModules(cards);
415
+ const selectedCards = cards.slice(0, MAX_KNOWLEDGE_INDEX_CARDS);
416
+ const result = {
417
+ artifactPath: relativeHarnessPath("knowledge", "index.json"),
418
+ cardCount: cards.length,
419
+ activeCardCount: cards.filter((card) => card.status === "active").length,
420
+ deprecatedCardCount: cards.filter((card) => card.status === "deprecated").length,
421
+ prdSourceCount: prdSources.length,
422
+ uncoveredPrdSourceCount: prdSources.filter((source) => source.activeCardCount === 0).length,
423
+ byType: countByType(cards),
424
+ byKind: countByKind(cards),
425
+ moduleSummaries,
426
+ prdSources,
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
+ }
435
+ };
436
+ writeJson(path.join(projectRoot, result.artifactPath), result);
437
+ return result;
438
+ }
439
+ export function checkKnowledgeCoverage(projectRoot, options = {}) {
440
+ const cards = discoverKnowledgeCards(projectRoot);
441
+ if (!isReadableDirectory(harnessPath(projectRoot, "knowledge"))) {
442
+ return {
443
+ status: "not_configured",
444
+ prdSourceCount: 0,
445
+ coveredPrdSourceCount: 0,
446
+ uncoveredPrdSources: [],
447
+ prdSourcesWithoutCoverageItems: [],
448
+ missingSourcePaths: [],
449
+ cardsWithoutSource: [],
450
+ cardsWithoutCoverage: [],
451
+ cardsWithBroadCoverage: [],
452
+ cardsWithTraceabilityDrift: [],
453
+ warnings: ["Create .frontend-harness/knowledge before checking project knowledge coverage."],
454
+ errors: []
455
+ };
456
+ }
457
+ const prdSources = summarizePrdSources(projectRoot, cards, options.requiredPrdSources ?? []);
458
+ const uncoveredPrdSources = prdSources
459
+ .filter((source) => source.activeCardCount === 0)
460
+ .map((source) => source.path);
461
+ const missingSourcePaths = [...new Set(cards.flatMap((card) => card.sourcePaths)
462
+ .filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)))].sort();
463
+ const cardsWithoutSource = cards
464
+ .filter((card) => card.status === "active" && card.type === "prd_semantics" && card.sourcePaths.length === 0)
465
+ .map((card) => card.path);
466
+ const cardsWithoutCoverage = cards
467
+ .filter((card) => card.status === "active" && isPrdTraceableCard(card) && card.coverage.length === 0)
468
+ .map((card) => card.path);
469
+ const cardsWithBroadCoverage = cards
470
+ .filter((card) => card.status === "active" && isPrdTraceableCard(card) && card.coverage.some(isBroadCoverageItem))
471
+ .map((card) => card.path);
472
+ const cardsWithTraceabilityDrift = cards
473
+ .filter((card) => card.status === "active" && isPrdTraceableCard(card) && hasCoverageBodyDrift(projectRoot, card))
474
+ .map((card) => card.path);
475
+ const prdSourcesWithoutCoverageItems = prdSources
476
+ .filter((source) => source.activeCardCount > 0 && source.coverage.length === 0)
477
+ .map((source) => source.path);
478
+ const warnings = [
479
+ ...cardsWithoutSource.map((cardPath) => `${cardPath} has no source_paths for PRD traceability.`),
480
+ ...cardsWithoutCoverage.map((cardPath) => `${cardPath} has no coverage items for PRD block traceability.`),
481
+ ...cardsWithBroadCoverage.map((cardPath) => `${cardPath} uses broad PRD coverage; split PRD semantics into atomic cards with precise section, table, rule, or acceptance-block coverage.`),
482
+ ...cardsWithTraceabilityDrift.map((cardPath) => `${cardPath} frontmatter coverage disagrees with body traceability.`),
483
+ ...prdSourcesWithoutCoverageItems.map((sourcePath) => `PRD source has cards but no coverage items: ${sourcePath}`),
484
+ ...missingSourcePaths.map((sourcePath) => `source path does not exist: ${sourcePath}`)
485
+ ];
486
+ const errors = [
487
+ ...uncoveredPrdSources.map((sourcePath) => `PRD source has no active knowledge cards: ${sourcePath}`),
488
+ ...prdSourcesWithoutCoverageItems.map((sourcePath) => `PRD source has no coverage items: ${sourcePath}`),
489
+ ...cardsWithBroadCoverage.map((cardPath) => `${cardPath} uses broad PRD coverage instead of atomic traceability.`),
490
+ ...cardsWithTraceabilityDrift.map((cardPath) => `${cardPath} has frontmatter coverage but body traceability says coverage is not specified.`),
491
+ ...missingSourcePaths.map((sourcePath) => `source path does not exist: ${sourcePath}`)
492
+ ];
493
+ return {
494
+ status: errors.length || cardsWithoutSource.length || cardsWithoutCoverage.length ? "failed" : "passed",
495
+ prdSourceCount: prdSources.length,
496
+ coveredPrdSourceCount: prdSources.filter((source) => source.activeCardCount > 0).length,
497
+ uncoveredPrdSources,
498
+ prdSourcesWithoutCoverageItems,
499
+ missingSourcePaths,
500
+ cardsWithoutSource,
501
+ cardsWithoutCoverage,
502
+ cardsWithBroadCoverage,
503
+ cardsWithTraceabilityDrift,
504
+ warnings,
505
+ errors
506
+ };
507
+ }
508
+ export function listKnowledge(projectRoot, options = {}) {
509
+ const cards = discoverKnowledgeCards(projectRoot)
510
+ .filter((card) => options.includeDeprecated || card.status === "active")
511
+ .map(stripSearchable);
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
+ };
521
+ }
522
+ export function searchKnowledge(projectRoot, query, options = {}) {
523
+ const normalizedQuery = normalize(query ?? "");
524
+ if (!normalizedQuery) {
525
+ throw new Error("knowledge search requires --query");
526
+ }
527
+ const limit = options.limit && options.limit > 0 ? options.limit : MAX_KNOWLEDGE_SEARCH_RESULTS;
528
+ const matches = discoverKnowledgeCards(projectRoot)
529
+ .filter((card) => options.includeDeprecated || card.status === "active")
530
+ .map((card) => ({
531
+ card,
532
+ score: scoreCardSearch(card, normalizedQuery)
533
+ }))
534
+ .filter((item) => item.score > 0)
535
+ .sort((left, right) => {
536
+ if (right.score !== left.score) {
537
+ return right.score - left.score;
538
+ }
539
+ return left.card.path.localeCompare(right.card.path);
540
+ });
541
+ const cards = matches
542
+ .slice(0, limit)
543
+ .map(({ card, score }) => ({
544
+ ...stripSearchable(card),
545
+ score
546
+ }));
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
+ };
556
+ }
557
+ export function showKnowledge(projectRoot, idOrPath) {
558
+ const target = idOrPath?.trim();
559
+ if (!target) {
560
+ throw new Error("knowledge show requires --id or --path");
561
+ }
562
+ const cards = discoverKnowledgeCards(projectRoot);
563
+ const card = resolveKnowledgeCardReference(cards, target);
564
+ if (!card) {
565
+ throw new Error(`knowledge card not found: ${target}`);
566
+ }
567
+ const fullPath = path.join(projectRoot, card.path);
568
+ const content = safeRead(fullPath);
569
+ if (content === null) {
570
+ throw new Error(`knowledge card cannot be read: ${card.path}`);
571
+ }
572
+ return {
573
+ card: stripSearchable(card),
574
+ body: extractMarkdownBody(content)
575
+ };
576
+ }
577
+ function sameKnowledgeSourceReference(left, right) {
578
+ return left.replace(/\\/g, "/").toLowerCase() === right.replace(/\\/g, "/").toLowerCase();
579
+ }
580
+ export function checkKnowledge(projectRoot) {
581
+ const knowledgeRoot = harnessPath(projectRoot, "knowledge");
582
+ const relativeRoot = path.relative(projectRoot, knowledgeRoot).split(path.sep).join(path.posix.sep);
583
+ if (!fs.existsSync(knowledgeRoot)) {
584
+ return {
585
+ status: "not_configured",
586
+ knowledgeRoot: relativeRoot,
587
+ cardCount: 0,
588
+ activeCardCount: 0,
589
+ deprecatedCardCount: 0,
590
+ errors: [],
591
+ warnings: ["Create .frontend-harness/knowledge before checking project knowledge."]
592
+ };
593
+ }
594
+ const errors = [];
595
+ const warnings = [];
596
+ if (!isReadableDirectory(knowledgeRoot)) {
597
+ errors.push(".frontend-harness/knowledge must be a readable directory.");
598
+ }
599
+ const files = isReadableDirectory(knowledgeRoot) ? listMarkdownFiles(knowledgeRoot, errors) : [];
600
+ const ids = new Set();
601
+ const duplicateIds = new Set();
602
+ let activeCardCount = 0;
603
+ let deprecatedCardCount = 0;
604
+ for (const fullPath of files) {
605
+ const relativePath = path.relative(projectRoot, fullPath).split(path.sep).join(path.posix.sep);
606
+ const content = safeRead(fullPath);
607
+ if (content === null) {
608
+ errors.push(`${relativePath} cannot be read.`);
609
+ continue;
610
+ }
611
+ const parsed = parseKnowledgeFrontmatter(content);
612
+ if (!parsed.hasFrontmatter) {
613
+ errors.push(`${relativePath} must include YAML frontmatter.`);
614
+ continue;
615
+ }
616
+ const cardErrors = validateKnowledgeMetadata(relativePath, parsed.metadata);
617
+ errors.push(...parsed.syntaxErrors.map((error) => `${relativePath} ${error}.`));
618
+ errors.push(...cardErrors);
619
+ const id = parsed.metadata.id;
620
+ if (id) {
621
+ if (ids.has(id)) {
622
+ duplicateIds.add(id);
623
+ }
624
+ ids.add(id);
625
+ }
626
+ if (parsed.metadata.status === "active") {
627
+ activeCardCount += 1;
628
+ }
629
+ if (parsed.metadata.status === "deprecated") {
630
+ deprecatedCardCount += 1;
631
+ if (!parsed.metadata.related.length) {
632
+ warnings.push(`${relativePath} is deprecated without a related replacement or explanation.`);
633
+ }
634
+ }
635
+ if ((parsed.metadata.summary?.length ?? 0) > 180) {
636
+ warnings.push(`${relativePath} summary should stay concise for context display.`);
637
+ }
638
+ if (parsed.metadata.kind) {
639
+ const contentValue = primaryAtomicValue(parsed.metadata);
640
+ if (contentValue && normalize(contentValue) === normalize(parsed.metadata.summary ?? "")) {
641
+ warnings.push(`${relativePath} summary duplicates the primary atomic content; use summary for context and the primary field for the durable rule.`);
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
+ }
652
+ if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.length === 0) {
653
+ warnings.push(`${relativePath} should include coverage items for PRD block traceability.`);
654
+ }
655
+ if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.some(isBroadCoverageItem)) {
656
+ warnings.push(`${relativePath} uses broad PRD coverage; split PRD semantics into smaller atomic cards with precise section, table, rule, or acceptance-block coverage.`);
657
+ }
658
+ }
659
+ if (parsed.metadata.coverage.length > 0 && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content))) {
660
+ warnings.push(`${relativePath} frontmatter coverage disagrees with body traceability.`);
661
+ }
662
+ for (const sourcePath of parsed.metadata.sourcePaths) {
663
+ if (sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)) {
664
+ warnings.push(`${relativePath} source path is missing or unsafe: ${sourcePath}.`);
665
+ }
666
+ }
667
+ }
668
+ for (const duplicateId of duplicateIds) {
669
+ errors.push(`Duplicate knowledge id: ${duplicateId}.`);
670
+ }
671
+ return {
672
+ status: errors.length ? "failed" : "passed",
673
+ knowledgeRoot: relativeRoot,
674
+ cardCount: files.length,
675
+ activeCardCount,
676
+ deprecatedCardCount,
677
+ errors,
678
+ warnings
679
+ };
680
+ }
681
+ function describeKnowledgeCard(projectRoot, fullPath) {
682
+ const relativePath = path.relative(projectRoot, fullPath).split(path.sep).join(path.posix.sep);
683
+ const content = safeRead(fullPath);
684
+ if (content === null) {
685
+ return null;
686
+ }
687
+ const parsed = parseKnowledgeFrontmatter(content);
688
+ if (!parsed.hasFrontmatter || parsed.syntaxErrors.length || validateKnowledgeMetadata(relativePath, parsed.metadata).length) {
689
+ return null;
690
+ }
691
+ const metadata = parsed.metadata;
692
+ const body = extractMarkdownBody(content);
693
+ const title = metadata.title;
694
+ const summary = metadata.summary;
695
+ const type = metadata.type;
696
+ const kind = metadata.kind;
697
+ const status = metadata.status;
698
+ const stability = metadata.stability;
699
+ const scope = metadata.scope;
700
+ const tags = metadata.tags;
701
+ const sourcePaths = metadata.sourcePaths;
702
+ const related = metadata.related;
703
+ const updatedAt = metadata.updatedAt;
704
+ return {
705
+ id: metadata.id,
706
+ path: relativePath,
707
+ title,
708
+ summary,
709
+ type,
710
+ ...(kind ? { kind } : {}),
711
+ ...(metadata.subject ? { subject: metadata.subject } : {}),
712
+ ...(metadata.rule ? { rule: metadata.rule } : {}),
713
+ ...(metadata.workflow ? { workflow: metadata.workflow } : {}),
714
+ ...(metadata.permission ? { permission: metadata.permission } : {}),
715
+ ...(metadata.term ? { term: metadata.term } : {}),
716
+ ...(metadata.note ? { note: metadata.note } : {}),
717
+ verification: metadata.verification,
718
+ coverage: metadata.coverage,
719
+ prdAnchors: metadata.prdAnchors,
720
+ status,
721
+ stability,
722
+ scope,
723
+ tags,
724
+ sourcePaths,
725
+ related,
726
+ updatedAt,
727
+ searchable: normalize([
728
+ relativePath,
729
+ metadata.id,
730
+ title,
731
+ summary,
732
+ type,
733
+ kind ?? "",
734
+ stability,
735
+ metadata.subject ?? "",
736
+ metadata.rule ?? "",
737
+ metadata.workflow ?? "",
738
+ metadata.permission ?? "",
739
+ metadata.term ?? "",
740
+ metadata.note ?? "",
741
+ ...metadata.verification,
742
+ ...metadata.coverage,
743
+ ...metadata.prdAnchors,
744
+ ...scope,
745
+ ...tags,
746
+ ...sourcePaths,
747
+ ...related,
748
+ body
749
+ ].join(" "))
750
+ };
751
+ }
752
+ function stripSearchable(card) {
753
+ const { searchable, ...publicCard } = card;
754
+ void searchable;
755
+ return publicCard;
756
+ }
757
+ function resolveKnowledgeCardReference(cards, target) {
758
+ const exactPath = cards.find((item) => item.path === target);
759
+ if (exactPath) {
760
+ return exactPath;
761
+ }
762
+ const idMatches = cards.filter((item) => item.id === target);
763
+ if (idMatches.length === 1) {
764
+ return idMatches[0] ?? null;
765
+ }
766
+ if (idMatches.length > 1) {
767
+ throw new Error(formatAmbiguousKnowledgeReference(target, idMatches));
768
+ }
769
+ const pathSuffixMatches = cards.filter((item) => item.path.endsWith(`/${target}`));
770
+ if (pathSuffixMatches.length === 1) {
771
+ return pathSuffixMatches[0] ?? null;
772
+ }
773
+ if (pathSuffixMatches.length > 1) {
774
+ throw new Error(formatAmbiguousKnowledgeReference(target, pathSuffixMatches));
775
+ }
776
+ return null;
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
+ }
783
+ function parseKnowledgeFrontmatter(content) {
784
+ const metadata = {
785
+ scope: [],
786
+ tags: [],
787
+ verification: [],
788
+ coverage: [],
789
+ prdAnchors: [],
790
+ sourcePaths: [],
791
+ related: []
792
+ };
793
+ const syntaxErrors = [];
794
+ if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
795
+ return { hasFrontmatter: false, syntaxErrors, metadata };
796
+ }
797
+ const endIndex = content.search(/\r?\n---(?:\r?\n|$)/);
798
+ if (endIndex < 0) {
799
+ syntaxErrors.push("frontmatter must be closed with ---");
800
+ return { hasFrontmatter: true, syntaxErrors, metadata };
801
+ }
802
+ const listKeys = new Set(["scope", "tags", "verification", "coverage", "prd_anchors", "source_paths", "related"]);
803
+ let currentList = null;
804
+ const startIndex = content.startsWith("---\r\n") ? 5 : 4;
805
+ for (const [lineIndex, line] of content.slice(startIndex, endIndex).split(/\r?\n/).entries()) {
806
+ const trimmed = line.trim();
807
+ if (!trimmed || trimmed.startsWith("#")) {
808
+ continue;
809
+ }
810
+ const leadingWhitespace = line.match(/^\s*/)?.[0] ?? "";
811
+ if (leadingWhitespace.length > 0 && !trimmed.startsWith("-") && /^[A-Za-z][A-Za-z0-9_-]*:\s*/.test(trimmed)) {
812
+ syntaxErrors.push(`frontmatter line ${lineIndex + 2} has an indented mapping key: ${trimmed}`);
813
+ }
814
+ if (trimmed === "[]" && currentList) {
815
+ currentList = null;
816
+ continue;
817
+ }
818
+ const listItem = trimmed.match(/^-\s+(.+)$/);
819
+ if (listItem) {
820
+ if (!currentList) {
821
+ syntaxErrors.push(`frontmatter line ${lineIndex + 2} has a list item without a list key`);
822
+ continue;
823
+ }
824
+ const value = listItem[1];
825
+ if (value) {
826
+ metadata[currentList].push(unquoteScalar(value));
827
+ }
828
+ continue;
829
+ }
830
+ const pair = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
831
+ if (!pair) {
832
+ currentList = null;
833
+ syntaxErrors.push(`frontmatter line ${lineIndex + 2} must be a key/value pair or list item`);
834
+ continue;
835
+ }
836
+ const key = pair[1] ?? "";
837
+ const value = (pair[2] ?? "").trim();
838
+ if (isKnowledgeScalarKey(key)) {
839
+ metadata[key] = unquoteScalar(value);
840
+ currentList = null;
841
+ continue;
842
+ }
843
+ if (key === "updated_at") {
844
+ metadata.updatedAt = unquoteScalar(value);
845
+ currentList = null;
846
+ continue;
847
+ }
848
+ if (listKeys.has(key)) {
849
+ const mappedKey = key === "source_paths"
850
+ ? "sourcePaths"
851
+ : key === "prd_anchors"
852
+ ? "prdAnchors"
853
+ : key;
854
+ metadata[mappedKey] = parseListValue(value);
855
+ currentList = value ? null : mappedKey;
856
+ continue;
857
+ }
858
+ currentList = null;
859
+ syntaxErrors.push(`frontmatter line ${lineIndex + 2} uses unsupported field: ${key}`);
860
+ }
861
+ return { hasFrontmatter: true, syntaxErrors, metadata };
862
+ }
863
+ function isKnowledgeScalarKey(key) {
864
+ return key === "id"
865
+ || key === "title"
866
+ || key === "summary"
867
+ || key === "type"
868
+ || key === "kind"
869
+ || key === "subject"
870
+ || key === "rule"
871
+ || key === "workflow"
872
+ || key === "permission"
873
+ || key === "term"
874
+ || key === "note"
875
+ || key === "status"
876
+ || key === "stability";
877
+ }
878
+ function validateKnowledgeMetadata(relativePath, metadata) {
879
+ const errors = [];
880
+ if (!metadata.id || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(metadata.id)) {
881
+ errors.push(`${relativePath} must include a kebab-case id.`);
882
+ }
883
+ if (!metadata.title) {
884
+ errors.push(`${relativePath} must include a non-empty title.`);
885
+ }
886
+ if (!metadata.summary) {
887
+ errors.push(`${relativePath} must include a non-empty summary.`);
888
+ }
889
+ if (!metadata.type || !knowledgeTypes.has(metadata.type)) {
890
+ errors.push(`${relativePath} type must be one of: ${[...knowledgeTypes].join(", ")}.`);
891
+ }
892
+ if (metadata.kind) {
893
+ if (!knowledgeKinds.has(metadata.kind)) {
894
+ errors.push(`${relativePath} kind must be one of: ${[...knowledgeKinds].join(", ")}.`);
895
+ }
896
+ if (!metadata.subject) {
897
+ errors.push(`${relativePath} atomic knowledge must include subject.`);
898
+ }
899
+ if (!metadata.rule && !metadata.workflow && !metadata.permission && !metadata.term && !metadata.note) {
900
+ errors.push(`${relativePath} atomic knowledge must include rule, workflow, permission, term, or note.`);
901
+ }
902
+ }
903
+ if (!metadata.status || !knowledgeStatuses.has(metadata.status)) {
904
+ errors.push(`${relativePath} status must be one of: ${[...knowledgeStatuses].join(", ")}.`);
905
+ }
906
+ if (!metadata.stability || !knowledgeStabilities.has(metadata.stability)) {
907
+ errors.push(`${relativePath} stability must be one of: ${[...knowledgeStabilities].join(", ")}.`);
908
+ }
909
+ if (!metadata.updatedAt || !/^\d{4}-\d{2}-\d{2}([T ][0-9:.+-Z]+)?$/.test(metadata.updatedAt)) {
910
+ errors.push(`${relativePath} must include updated_at as YYYY-MM-DD or ISO timestamp.`);
911
+ }
912
+ for (const sourcePath of metadata.sourcePaths) {
913
+ const sourceError = validateKnowledgeSourcePath(sourcePath);
914
+ if (sourceError) {
915
+ errors.push(`${relativePath} source_paths must contain safe project-relative paths or external references: ${sourcePath}.`);
916
+ }
917
+ }
918
+ return errors;
919
+ }
920
+ function renderKnowledgeFromMetadata(metadata, body) {
921
+ const renderedBody = renderBodyWithTraceability(body, metadata.coverage ?? [], metadata.prdAnchors ?? [], metadata.verification ?? []);
922
+ return `---
923
+ id: ${yamlScalar(metadata.id)}
924
+ type: ${yamlScalar(metadata.type)}
925
+ ${renderOptionalScalar("kind", metadata.kind)}title: ${yamlScalar(metadata.title)}
926
+ summary: ${yamlScalar(metadata.summary)}
927
+ ${renderOptionalScalar("subject", metadata.subject)}${renderOptionalScalar("rule", metadata.rule)}${renderOptionalScalar("workflow", metadata.workflow)}${renderOptionalScalar("permission", metadata.permission)}${renderOptionalScalar("term", metadata.term)}${renderOptionalScalar("note", metadata.note)}scope:
928
+ ${renderList(metadata.scope)}
929
+ tags:
930
+ ${renderList(metadata.tags)}
931
+ verification:
932
+ ${renderList(metadata.verification)}
933
+ coverage:
934
+ ${renderList(metadata.coverage ?? [])}
935
+ prd_anchors:
936
+ ${renderList(metadata.prdAnchors ?? [])}
937
+ status: ${yamlScalar(metadata.status)}
938
+ stability: ${yamlScalar(metadata.stability)}
939
+ updated_at: ${yamlScalar(metadata.updatedAt)}
940
+ source_paths:
941
+ ${renderList(metadata.sourcePaths)}
942
+ related:
943
+ ${renderList(metadata.related)}
944
+ ---
945
+
946
+ ${renderedBody}
947
+ `;
948
+ }
949
+ function renderBodyWithTraceability(body, coverage, prdAnchors, verification) {
950
+ const sections = [body.trim()];
951
+ if ((coverage.length || prdAnchors.length) && !/^## Traceability\b/m.test(body)) {
952
+ sections.push([
953
+ "## Traceability",
954
+ "",
955
+ ...(coverage.length ? coverage.map((item) => `- Coverage: ${item}`) : ["- Coverage: Not specified."]),
956
+ ...(prdAnchors.length ? prdAnchors.map((item) => `- Anchor: ${item}`) : [])
957
+ ].join("\n"));
958
+ }
959
+ if (verification.length && !/^## Verification\b/m.test(body)) {
960
+ sections.push([
961
+ "## Verification",
962
+ "",
963
+ ...verification.map((item) => `- ${item}`)
964
+ ].join("\n"));
965
+ }
966
+ return sections.join("\n\n");
967
+ }
968
+ function renderAtomicKnowledge(input) {
969
+ return `---
970
+ id: ${yamlScalar(input.id)}
971
+ type: ${yamlScalar(input.type)}
972
+ kind: ${yamlScalar(input.kind)}
973
+ title: ${yamlScalar(input.title)}
974
+ summary: ${yamlScalar(input.summary)}
975
+ subject: ${yamlScalar(input.subject)}
976
+ ${renderOptionalScalar("rule", input.contentFields.rule)}${renderOptionalScalar("workflow", input.contentFields.workflow)}${renderOptionalScalar("permission", input.contentFields.permission)}${renderOptionalScalar("term", input.contentFields.term)}${renderOptionalScalar("note", input.contentFields.note)}scope:
977
+ ${renderList(input.scope)}
978
+ tags:
979
+ ${renderList(input.tags)}
980
+ verification:
981
+ ${renderList(input.verification)}
982
+ coverage:
983
+ ${renderList(input.coverage)}
984
+ prd_anchors:
985
+ ${renderList(input.prdAnchors)}
986
+ status: ${yamlScalar(input.status)}
987
+ stability: ${yamlScalar(input.stability)}
988
+ updated_at: ${yamlScalar(input.createdAt)}
989
+ source_paths:
990
+ ${renderList(input.sourcePaths)}
991
+ related: []
992
+ ---
993
+
994
+ # ${input.title}
995
+
996
+ ${renderAtomicBody(input.kind, input.subject, input.summary, input.contentFields, input.coverage, input.prdAnchors, input.verification)}
997
+ `;
998
+ }
999
+ function renderOptionalScalar(key, value) {
1000
+ return value ? `${key}: ${yamlScalar(value)}\n` : "";
1001
+ }
1002
+ function renderAtomicBody(kind, subject, summary, contentFields, coverage, prdAnchors, verification) {
1003
+ const contentEntries = Object.entries(contentFields).filter(([, value]) => Boolean(value?.trim()));
1004
+ const primaryLabel = titleCase(contentKeyForKind(kind));
1005
+ const primaryValue = contentFields[contentKeyForKind(kind)] ?? contentEntries[0]?.[1];
1006
+ const sections = [
1007
+ "# " + titleCase(kind) + ": " + subject,
1008
+ "",
1009
+ "## Context",
1010
+ "",
1011
+ summary,
1012
+ "",
1013
+ "## Durable Knowledge",
1014
+ "",
1015
+ ...(primaryValue ? [`**${primaryLabel}**`, "", primaryValue] : [subject])
1016
+ ];
1017
+ const note = contentFields.note;
1018
+ if (note && contentKeyForKind(kind) !== "note") {
1019
+ sections.push("", "## Notes", "", note);
1020
+ }
1021
+ sections.push("", "## Traceability", "", ...(coverage.length ? coverage.map((item) => `- Coverage: ${item}`) : ["- Coverage: Not specified."]), ...(prdAnchors.length ? prdAnchors.map((item) => `- Anchor: ${item}`) : []));
1022
+ sections.push("", "## Verification", "", ...(verification.length ? verification.map((item) => `- ${item}`) : ["- Not specified."]));
1023
+ return [
1024
+ ...sections
1025
+ ].join("\n");
1026
+ }
1027
+ function renderKnowledge(input) {
1028
+ const summary = firstBodyLine(input.body) ?? input.title;
1029
+ return `---
1030
+ id: ${yamlScalar(input.id)}
1031
+ type: ${yamlScalar(input.type)}
1032
+ title: ${yamlScalar(input.title)}
1033
+ summary: ${yamlScalar(summary)}
1034
+ scope:
1035
+ ${renderList(input.scope)}
1036
+ tags:
1037
+ ${renderList(input.tags)}
1038
+ status: ${yamlScalar(input.status)}
1039
+ stability: ${yamlScalar(input.stability)}
1040
+ updated_at: ${yamlScalar(input.createdAt)}
1041
+ source_paths:
1042
+ ${renderList(input.sourcePaths)}
1043
+ related: []
1044
+ ---
1045
+
1046
+ # ${input.title}
1047
+
1048
+ ${input.body}
1049
+ `;
1050
+ }
1051
+ function renderList(items) {
1052
+ return items.length ? items.map((item) => ` - ${yamlScalar(item)}`).join("\n") : " []";
1053
+ }
1054
+ function firstBodyLine(body) {
1055
+ return body
1056
+ .split("\n")
1057
+ .map((line) => line.trim())
1058
+ .find((line) => line && !line.startsWith("#") && !line.startsWith("- ")) ?? null;
1059
+ }
1060
+ function parseKnowledgeType(value, label) {
1061
+ if (knowledgeTypes.has(value)) {
1062
+ return value;
1063
+ }
1064
+ throw new Error(`${label} must be one of: ${[...knowledgeTypes].join(", ")}`);
1065
+ }
1066
+ function parseKnowledgeStatus(value, label) {
1067
+ if (knowledgeStatuses.has(value)) {
1068
+ return value;
1069
+ }
1070
+ throw new Error(`${label} must be one of: ${[...knowledgeStatuses].join(", ")}`);
1071
+ }
1072
+ function parseKnowledgeStability(value, label) {
1073
+ if (knowledgeStabilities.has(value)) {
1074
+ return value;
1075
+ }
1076
+ throw new Error(`${label} must be one of: ${[...knowledgeStabilities].join(", ")}`);
1077
+ }
1078
+ function parseKnowledgeKind(value, label) {
1079
+ if (knowledgeKinds.has(value)) {
1080
+ return value;
1081
+ }
1082
+ throw new Error(`${label} must be one of: ${[...knowledgeKinds].join(", ")}`);
1083
+ }
1084
+ function atomicContent(kind, input) {
1085
+ return atomicContentFields(kind, input)[contentKeyForKind(kind)] ?? input.note?.trim() ?? null;
1086
+ }
1087
+ function atomicContentFields(kind, input) {
1088
+ const fields = {};
1089
+ const preferredKey = contentKeyForKind(kind);
1090
+ const preferredValue = input[preferredKey]?.trim();
1091
+ if (preferredValue) {
1092
+ fields[preferredKey] = preferredValue;
1093
+ }
1094
+ const note = input.note?.trim();
1095
+ if (note && preferredKey !== "note") {
1096
+ fields.note = note;
1097
+ }
1098
+ return fields;
1099
+ }
1100
+ function summarizeAtomicContent(kind, subject, contentFields) {
1101
+ const primary = contentFields[contentKeyForKind(kind)] ?? contentFields.note ?? subject;
1102
+ const concise = primary.length > 140 ? `${primary.slice(0, 137)}...` : primary;
1103
+ return `${subject}: ${concise}`;
1104
+ }
1105
+ function contentKeyForKind(kind) {
1106
+ if (kind === "workflow") {
1107
+ return "workflow";
1108
+ }
1109
+ if (kind === "permission") {
1110
+ return "permission";
1111
+ }
1112
+ if (kind === "term") {
1113
+ return "term";
1114
+ }
1115
+ if (kind === "pitfall" || kind === "decision" || kind === "convention") {
1116
+ return "note";
1117
+ }
1118
+ return "rule";
1119
+ }
1120
+ function primaryAtomicValue(metadata) {
1121
+ const kind = metadata.kind;
1122
+ if (!kind || !knowledgeKinds.has(kind)) {
1123
+ return undefined;
1124
+ }
1125
+ return metadata[contentKeyForKind(kind)] ?? metadata.note;
1126
+ }
1127
+ function contentFlagForKind(kind) {
1128
+ return contentKeyForKind(kind);
1129
+ }
1130
+ function splitList(value) {
1131
+ return value
1132
+ ? value.split(",").map((item) => item.trim()).filter(Boolean)
1133
+ : [];
1134
+ }
1135
+ function parseKnowledgeSourcePaths(value, label) {
1136
+ const sourcePaths = splitList(value);
1137
+ const invalid = sourcePaths.find(validateKnowledgeSourcePath);
1138
+ if (invalid) {
1139
+ throw new Error(`${label} must contain safe project-relative paths or external references: ${invalid}`);
1140
+ }
1141
+ return sourcePaths;
1142
+ }
1143
+ function mergeKnowledgeSourcePaths(existing, next, label) {
1144
+ if (next === undefined) {
1145
+ return existing;
1146
+ }
1147
+ const merged = [...new Set([...existing, ...splitList(next)])];
1148
+ const invalid = merged.find(validateKnowledgeSourcePath);
1149
+ if (invalid) {
1150
+ throw new Error(`${label} must contain safe project-relative paths or external references: ${invalid}`);
1151
+ }
1152
+ return merged;
1153
+ }
1154
+ function validateKnowledgeSourcePath(value) {
1155
+ if (!value.trim()) {
1156
+ return "empty";
1157
+ }
1158
+ if (value.toLowerCase().startsWith("ingest://")) {
1159
+ return isIngestReference(value) ? null : "unsafe";
1160
+ }
1161
+ if (isExternalReference(value)) {
1162
+ return null;
1163
+ }
1164
+ try {
1165
+ normalizeProjectRelativePath(value, "knowledge source path");
1166
+ return null;
1167
+ }
1168
+ catch {
1169
+ return "unsafe";
1170
+ }
1171
+ }
1172
+ function isKnowledgeSourceFile(projectRoot, sourcePath) {
1173
+ return isRegularFileWithoutSymlink(path.join(projectRoot, sourcePath));
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
+ }
1182
+ function mergeList(existing, next) {
1183
+ if (next === undefined) {
1184
+ return existing;
1185
+ }
1186
+ return [...new Set([...existing, ...splitList(next)])];
1187
+ }
1188
+ function parseListValue(value) {
1189
+ if (!value || value === "[]") {
1190
+ return [];
1191
+ }
1192
+ const inline = value.match(/^\[(.*)\]$/);
1193
+ if (!inline) {
1194
+ return [unquoteScalar(value)];
1195
+ }
1196
+ return splitInlineListItems(inline[1] ?? "")
1197
+ .map((item) => unquoteScalar(item.trim()))
1198
+ .filter(Boolean);
1199
+ }
1200
+ function splitInlineListItems(value) {
1201
+ const items = [];
1202
+ let current = "";
1203
+ let quote = null;
1204
+ let escaped = false;
1205
+ for (const char of value) {
1206
+ if (escaped) {
1207
+ current += char;
1208
+ escaped = false;
1209
+ continue;
1210
+ }
1211
+ if (char === "\\" && quote === "\"") {
1212
+ current += char;
1213
+ escaped = true;
1214
+ continue;
1215
+ }
1216
+ if (quote) {
1217
+ current += char;
1218
+ if (char === quote) {
1219
+ quote = null;
1220
+ }
1221
+ continue;
1222
+ }
1223
+ if (char === "\"" || char === "'") {
1224
+ current += char;
1225
+ quote = char;
1226
+ continue;
1227
+ }
1228
+ if (char === ",") {
1229
+ items.push(current);
1230
+ current = "";
1231
+ continue;
1232
+ }
1233
+ current += char;
1234
+ }
1235
+ items.push(current);
1236
+ return items;
1237
+ }
1238
+ function unquoteScalar(value) {
1239
+ const trimmed = value.trim();
1240
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
1241
+ try {
1242
+ const parsed = JSON.parse(trimmed);
1243
+ return typeof parsed === "string" ? parsed : trimmed.slice(1, -1);
1244
+ }
1245
+ catch {
1246
+ return trimmed.slice(1, -1);
1247
+ }
1248
+ }
1249
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
1250
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1251
+ }
1252
+ return trimmed;
1253
+ }
1254
+ function slugify(value) {
1255
+ return value
1256
+ .toLowerCase()
1257
+ .replace(/[^a-z0-9]+/g, "-")
1258
+ .replace(/^-+|-+$/g, "")
1259
+ .slice(0, 80);
1260
+ }
1261
+ function titleCase(value) {
1262
+ return value.slice(0, 1).toUpperCase() + value.slice(1);
1263
+ }
1264
+ function formatKnowledgeStamp(createdAt) {
1265
+ return createdAt
1266
+ .replace(/[-:]/g, "")
1267
+ .replace(/\.(\d{3})Z$/, "$1Z");
1268
+ }
1269
+ function yamlScalar(value) {
1270
+ return JSON.stringify(value);
1271
+ }
1272
+ function safeRead(filePath) {
1273
+ if (!isRegularFileWithoutSymlink(filePath)) {
1274
+ return null;
1275
+ }
1276
+ try {
1277
+ const stat = fs.statSync(filePath);
1278
+ if (stat.size > MAX_KNOWLEDGE_MARKDOWN_BYTES) {
1279
+ return null;
1280
+ }
1281
+ return fs.readFileSync(filePath, "utf8");
1282
+ }
1283
+ catch {
1284
+ return null;
1285
+ }
1286
+ }
1287
+ function isReadableDirectory(directory) {
1288
+ return isDirectoryWithoutSymlink(directory);
1289
+ }
1290
+ function listMarkdownFiles(directory, errors, depth = 0, state = { count: 0, stopped: false }) {
1291
+ if (state.stopped) {
1292
+ return [];
1293
+ }
1294
+ if (depth > MAX_KNOWLEDGE_DIRECTORY_DEPTH) {
1295
+ errors?.push(`Knowledge markdown scan exceeded maximum directory depth at ${displayScanPath(directory)}.`);
1296
+ return [];
1297
+ }
1298
+ try {
1299
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
1300
+ const fullPath = path.join(directory, entry.name);
1301
+ if (entry.isSymbolicLink()) {
1302
+ return [];
1303
+ }
1304
+ if (entry.isDirectory()) {
1305
+ if (entry.name.startsWith(".")) {
1306
+ return [];
1307
+ }
1308
+ return listMarkdownFiles(fullPath, errors, depth + 1, state);
1309
+ }
1310
+ if (!entry.name.endsWith(".md") || !isRegularFileWithoutSymlink(fullPath)) {
1311
+ return [];
1312
+ }
1313
+ state.count += 1;
1314
+ if (state.count > MAX_KNOWLEDGE_MARKDOWN_FILES) {
1315
+ if (!state.stopped) {
1316
+ errors?.push(`Knowledge markdown scan exceeded ${MAX_KNOWLEDGE_MARKDOWN_FILES} files.`);
1317
+ }
1318
+ state.stopped = true;
1319
+ return [];
1320
+ }
1321
+ const stat = fs.statSync(fullPath, { throwIfNoEntry: false });
1322
+ if (stat && stat.size > MAX_KNOWLEDGE_MARKDOWN_BYTES) {
1323
+ errors?.push(`${displayScanPath(fullPath)} exceeds ${MAX_KNOWLEDGE_MARKDOWN_BYTES} bytes.`);
1324
+ return [];
1325
+ }
1326
+ return [fullPath];
1327
+ });
1328
+ }
1329
+ catch (error) {
1330
+ if (errors) {
1331
+ const message = error instanceof Error ? error.message : String(error);
1332
+ errors.push(`Cannot read .frontend-harness/knowledge: ${message}`);
1333
+ }
1334
+ return [];
1335
+ }
1336
+ }
1337
+ function displayScanPath(filePath) {
1338
+ const relativePath = path.relative(process.cwd(), filePath);
1339
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
1340
+ return relativePath.split(path.sep).join(path.posix.sep);
1341
+ }
1342
+ return filePath;
1343
+ }
1344
+ function normalize(value) {
1345
+ return value.toLowerCase().replace(/[^\p{L}\p{N}-]+/gu, " ").trim();
1346
+ }
1347
+ function scoreCardSearch(card, normalizedQuery) {
1348
+ const queryTokens = tokens(normalizedQuery);
1349
+ if (!queryTokens.length) {
1350
+ return card.searchable.includes(normalizedQuery) ? 1 : 0;
1351
+ }
1352
+ const searchableTokens = new Set(tokens(card.searchable));
1353
+ let score = 0;
1354
+ for (const token of queryTokens) {
1355
+ if (searchableTokens.has(token)) {
1356
+ score += 2;
1357
+ }
1358
+ else if (card.searchable.includes(token)) {
1359
+ score += 1;
1360
+ }
1361
+ }
1362
+ if (card.id === normalizedQuery || normalize(card.title) === normalizedQuery) {
1363
+ score += 4;
1364
+ }
1365
+ return score;
1366
+ }
1367
+ function tokens(value) {
1368
+ return value.split(/\s+/).filter((token) => token.length >= 2);
1369
+ }
1370
+ function extractMarkdownBody(content) {
1371
+ const endIndex = content.search(/\r?\n---(?:\r?\n|$)/);
1372
+ if (!content.startsWith("---") || endIndex < 0) {
1373
+ return content.trim();
1374
+ }
1375
+ return content.slice(endIndex).replace(/^\r?\n---\r?\n?/, "").trim();
1376
+ }
1377
+ function isExternalReference(value) {
1378
+ if (value.toLowerCase().startsWith("ingest://")) {
1379
+ return false;
1380
+ }
1381
+ if (/^[A-Za-z]:/.test(value)) {
1382
+ return false;
1383
+ }
1384
+ return /^[a-z][a-z0-9+.-]*:/i.test(value);
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
+ }
1401
+ function isBroadCoverageItem(value) {
1402
+ const normalized = value.trim().toLowerCase();
1403
+ if (!normalized) {
1404
+ return false;
1405
+ }
1406
+ if (/\bsections?\s+\d+(?:\.\d+)?\s*[-–—~]\s*\d+(?:\.\d+)?\b/.test(normalized)) {
1407
+ return true;
1408
+ }
1409
+ if (/(?:^|[#\s/])\d+(?:\.\d+)?\s*[-–—~至到]\s*\d+(?:\.\d+)?(?:\D|$)/.test(normalized)) {
1410
+ return true;
1411
+ }
1412
+ return /\b(chapter|section)\b.*\b(full|entire|whole|all)\b/.test(normalized);
1413
+ }
1414
+ function hasCoverageBodyDrift(projectRoot, card) {
1415
+ if (card.coverage.length === 0) {
1416
+ return false;
1417
+ }
1418
+ const content = safeRead(path.join(projectRoot, card.path));
1419
+ return content !== null && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content));
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
+ }
1450
+ function summarizePrdSources(projectRoot, cards, requiredSources = []) {
1451
+ const grouped = new Map();
1452
+ for (const sourcePath of discoverPrdSourceFiles(projectRoot)) {
1453
+ grouped.set(sourcePath, []);
1454
+ }
1455
+ for (const sourcePath of requiredSources.filter(isPrdSourcePath)) {
1456
+ grouped.set(sourcePath, grouped.get(sourcePath) ?? []);
1457
+ }
1458
+ for (const card of cards) {
1459
+ for (const sourcePath of card.sourcePaths.filter(isPrdSourcePath)) {
1460
+ grouped.set(sourcePath, [...(grouped.get(sourcePath) ?? []), card]);
1461
+ }
1462
+ }
1463
+ return [...grouped.entries()]
1464
+ .sort(([left], [right]) => left.localeCompare(right))
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
+ });
1489
+ }
1490
+ function summarizeModules(cards) {
1491
+ const atomicCards = cards.filter((card) => card.kind && card.status === "active");
1492
+ return cards
1493
+ .filter((card) => card.type === "module_summary" && card.status === "active")
1494
+ .map((moduleCard) => {
1495
+ const moduleSources = new Set(moduleCard.sourcePaths);
1496
+ const relatedIds = new Set(moduleCard.related);
1497
+ const relatedAtomicCards = atomicCards.filter((card) => relatedIds.has(card.id) ||
1498
+ card.related.includes(moduleCard.id) ||
1499
+ card.sourcePaths.some((sourcePath) => moduleSources.has(sourcePath)) ||
1500
+ card.scope.some((scope) => moduleCard.scope.includes(scope)));
1501
+ const atomicCardIds = relatedAtomicCards.map((card) => card.id).sort();
1502
+ return {
1503
+ id: moduleCard.id,
1504
+ path: moduleCard.path,
1505
+ title: moduleCard.title,
1506
+ summary: moduleCard.summary,
1507
+ sourcePaths: moduleCard.sourcePaths,
1508
+ scopes: moduleCard.scope,
1509
+ tags: moduleCard.tags,
1510
+ related: moduleCard.related,
1511
+ atomicCardCount: relatedAtomicCards.length,
1512
+ atomicCardIds: selectedIndexDetails(atomicCardIds),
1513
+ omittedAtomicCardIds: omittedIndexDetails(atomicCardIds)
1514
+ };
1515
+ })
1516
+ .sort((left, right) => left.path.localeCompare(right.path));
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
+ }
1524
+ function discoverPrdSourceFiles(projectRoot) {
1525
+ const roots = ["docs/prd", "docs/requirements", "prd", "requirements"];
1526
+ const rootSources = roots.flatMap((root) => {
1527
+ const fullPath = path.join(projectRoot, root);
1528
+ return isReadableDirectory(fullPath)
1529
+ ? listMarkdownFiles(fullPath).map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, "/"))
1530
+ : [];
1531
+ });
1532
+ const docsRoot = path.join(projectRoot, "docs");
1533
+ const docsNamedSources = isReadableDirectory(docsRoot)
1534
+ ? listMarkdownFiles(docsRoot)
1535
+ .map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, "/"))
1536
+ .filter(isPrdSourcePath)
1537
+ : [];
1538
+ return [...new Set([...rootSources, ...docsNamedSources])].sort();
1539
+ }
1540
+ function isPrdSourcePath(sourcePath) {
1541
+ const normalized = sourcePath.replace(/\\/g, "/");
1542
+ if (/^ingest:\/\/prd\//i.test(normalized)) {
1543
+ return isIngestReference(normalized);
1544
+ }
1545
+ if (/(^|\/)(prd|requirements?)(\/|[-_.])|(^|\/)docs\/(prd|requirements?)(\/|$)/i.test(normalized)) {
1546
+ return true;
1547
+ }
1548
+ return /(^|\/)docs\/[^/]*(?:prd|requirement|需求)[^/]*\.md$/i.test(normalized);
1549
+ }
1550
+ function isPrdTraceableCard(card) {
1551
+ return card.type === "prd_semantics" || card.type === "module_summary" || card.type === "pitfall" || card.type === "glossary";
1552
+ }
1553
+ function countByType(cards) {
1554
+ const counts = Object.fromEntries([...knowledgeTypes].map((type) => [type, 0]));
1555
+ for (const card of cards) {
1556
+ counts[card.type] += 1;
1557
+ }
1558
+ return counts;
1559
+ }
1560
+ function countByKind(cards) {
1561
+ const counts = {};
1562
+ for (const card of cards) {
1563
+ if (card.kind) {
1564
+ counts[card.kind] = (counts[card.kind] ?? 0) + 1;
1565
+ }
1566
+ }
1567
+ return counts;
1568
+ }
1569
+ //# sourceMappingURL=core.js.map