frontend-harness 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/AGENTS.md +8 -0
  2. package/CLAUDE.md +8 -0
  3. package/README.md +9 -0
  4. package/dist/cli/index.js +134 -5
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/runtime/builtin-skills.js +10 -1
  7. package/dist/runtime/builtin-skills.js.map +1 -1
  8. package/dist/runtime/command-taxonomy.js +8 -0
  9. package/dist/runtime/command-taxonomy.js.map +1 -1
  10. package/dist/runtime/common/text.d.ts +1 -0
  11. package/dist/runtime/common/text.js +4 -0
  12. package/dist/runtime/common/text.js.map +1 -0
  13. package/dist/runtime/graph.js +1 -1
  14. package/dist/runtime/graph.js.map +1 -1
  15. package/dist/runtime/knowledge.d.ts +136 -1
  16. package/dist/runtime/knowledge.js +642 -5
  17. package/dist/runtime/knowledge.js.map +1 -1
  18. package/dist/runtime/plan/component-resolver.js +19 -13
  19. package/dist/runtime/plan/component-resolver.js.map +1 -1
  20. package/dist/runtime/plan/guidance.js +24 -0
  21. package/dist/runtime/plan/guidance.js.map +1 -1
  22. package/dist/runtime/plan/workflow.js +31 -25
  23. package/dist/runtime/plan/workflow.js.map +1 -1
  24. package/dist/runtime/plan.js +55 -12
  25. package/dist/runtime/plan.js.map +1 -1
  26. package/dist/runtime/project-discovery.js +0 -1
  27. package/dist/runtime/project-discovery.js.map +1 -1
  28. package/dist/runtime/project-paths.js +2 -2
  29. package/dist/runtime/project-paths.js.map +1 -1
  30. package/dist/runtime/repair-decision.js +1 -3
  31. package/dist/runtime/repair-decision.js.map +1 -1
  32. package/dist/runtime/scaffold/vue-template.js +31 -11
  33. package/dist/runtime/scaffold/vue-template.js.map +1 -1
  34. package/dist/runtime/state.d.ts +4 -2
  35. package/dist/runtime/state.js +54 -6
  36. package/dist/runtime/state.js.map +1 -1
  37. package/dist/runtime/ui-restoration.d.ts +4 -0
  38. package/dist/runtime/ui-restoration.js +38 -0
  39. package/dist/runtime/ui-restoration.js.map +1 -0
  40. package/dist/runtime/units.js +2 -1
  41. package/dist/runtime/units.js.map +1 -1
  42. package/dist/runtime/verify.js +37 -29
  43. package/dist/runtime/verify.js.map +1 -1
  44. package/dist/schemas/types.d.ts +9 -0
  45. package/dist/schemas/validation.js +18 -1
  46. package/dist/schemas/validation.js.map +1 -1
  47. package/docs/DIRECTION.md +1 -1
  48. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { harnessPath, relativeHarnessPath } from "../storage/paths.js";
4
- import { writeText } from "../storage/json.js";
4
+ import { writeJson, writeText } from "../storage/json.js";
5
5
  const knowledgeTypes = new Set([
6
6
  "prd_semantics",
7
+ "module_summary",
7
8
  "project_convention",
8
9
  "decision_record",
9
10
  "pitfall",
@@ -11,6 +12,16 @@ const knowledgeTypes = new Set([
11
12
  ]);
12
13
  const knowledgeStatuses = new Set(["active", "deprecated"]);
13
14
  const knowledgeStabilities = new Set(["stable", "evolving"]);
15
+ const knowledgeKinds = new Set(["rule", "workflow", "permission", "term", "pitfall", "decision", "convention"]);
16
+ const kindTypeMap = {
17
+ rule: "prd_semantics",
18
+ workflow: "prd_semantics",
19
+ permission: "prd_semantics",
20
+ term: "glossary",
21
+ pitfall: "pitfall",
22
+ decision: "decision_record",
23
+ convention: "project_convention"
24
+ };
14
25
  export function promoteKnowledge(projectRoot, input) {
15
26
  const title = input.title?.trim();
16
27
  const body = input.body?.trim();
@@ -36,6 +47,7 @@ export function promoteKnowledge(projectRoot, input) {
36
47
  stability,
37
48
  createdAt,
38
49
  scope: splitList(input.scope),
50
+ tags: splitList(input.tags),
39
51
  sourcePaths: splitList(input.source)
40
52
  });
41
53
  writeText(path.join(projectRoot, relativePath), content);
@@ -49,6 +61,178 @@ export function promoteKnowledge(projectRoot, input) {
49
61
  createdAt
50
62
  };
51
63
  }
64
+ export function createModuleKnowledge(projectRoot, input) {
65
+ const title = input.title?.trim();
66
+ const summary = input.summary?.trim();
67
+ const body = input.body?.trim();
68
+ if (!title) {
69
+ throw new Error("knowledge module requires --title");
70
+ }
71
+ if (!summary) {
72
+ throw new Error("knowledge module requires --summary");
73
+ }
74
+ if (!body) {
75
+ throw new Error("knowledge module requires --body");
76
+ }
77
+ const status = parseKnowledgeStatus(input.status ?? "active", "knowledge module --status");
78
+ const stability = parseKnowledgeStability(input.stability ?? "stable", "knowledge module --stability");
79
+ const createdAt = new Date().toISOString();
80
+ const id = input.id?.trim() || slugify(title);
81
+ if (!id || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(id)) {
82
+ throw new Error("knowledge module --id must be kebab-case when provided");
83
+ }
84
+ const relativePath = relativeHarnessPath("knowledge", "modules", `${id}.md`);
85
+ const fullPath = path.join(projectRoot, relativePath);
86
+ if (fs.existsSync(fullPath)) {
87
+ throw new Error(`knowledge module already exists: ${relativePath}`);
88
+ }
89
+ const content = renderKnowledgeFromMetadata({
90
+ id,
91
+ title,
92
+ summary,
93
+ type: "module_summary",
94
+ status,
95
+ stability,
96
+ updatedAt: createdAt,
97
+ scope: splitList(input.scope),
98
+ tags: splitList(input.tags),
99
+ verification: [],
100
+ coverage: [],
101
+ prdAnchors: [],
102
+ sourcePaths: splitList(input.source),
103
+ related: splitList(input.related)
104
+ }, body);
105
+ writeText(fullPath, content);
106
+ return {
107
+ path: relativePath,
108
+ id,
109
+ title,
110
+ type: "module_summary",
111
+ status,
112
+ stability,
113
+ createdAt
114
+ };
115
+ }
116
+ export function addKnowledge(projectRoot, input) {
117
+ const kind = parseKnowledgeKind(input.kind ?? "", "knowledge add --kind");
118
+ const subject = input.subject?.trim();
119
+ if (!subject) {
120
+ throw new Error("knowledge add requires --subject");
121
+ }
122
+ const content = atomicContent(kind, input);
123
+ if (!content) {
124
+ throw new Error(`knowledge add requires --${contentFlagForKind(kind)} or --note`);
125
+ }
126
+ const status = parseKnowledgeStatus(input.status ?? "active", "knowledge add --status");
127
+ const stability = parseKnowledgeStability(input.stability ?? "stable", "knowledge add --stability");
128
+ const createdAt = new Date().toISOString();
129
+ const id = input.id?.trim() || slugify(`${subject}-${kind}-${content}`);
130
+ if (!id || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(id)) {
131
+ throw new Error("knowledge add --id must be kebab-case when provided");
132
+ }
133
+ const relativePath = relativeHarnessPath("knowledge", kind, `${id}.md`);
134
+ const fullPath = path.join(projectRoot, relativePath);
135
+ if (fs.existsSync(fullPath)) {
136
+ throw new Error(`knowledge card already exists: ${relativePath}`);
137
+ }
138
+ const title = `${subject} ${titleCase(kind)}`;
139
+ const contentFields = atomicContentFields(kind, input);
140
+ const inputSummary = input.summary?.trim();
141
+ const contentSummary = inputSummary || summarizeAtomicContent(kind, subject, contentFields);
142
+ const rendered = renderAtomicKnowledge({
143
+ id,
144
+ title,
145
+ summary: contentSummary,
146
+ type: kindTypeMap[kind],
147
+ kind,
148
+ subject,
149
+ status,
150
+ stability,
151
+ createdAt,
152
+ scope: splitList(input.scope),
153
+ tags: splitList(input.tags),
154
+ sourcePaths: splitList(input.source),
155
+ verification: splitList(input.verification),
156
+ coverage: splitList(input.coverage),
157
+ prdAnchors: splitList(input.anchor),
158
+ contentFields
159
+ });
160
+ writeText(fullPath, rendered);
161
+ return {
162
+ path: relativePath,
163
+ id,
164
+ title,
165
+ type: kindTypeMap[kind],
166
+ status,
167
+ stability,
168
+ createdAt
169
+ };
170
+ }
171
+ export function updateKnowledge(projectRoot, input) {
172
+ const target = input.id?.trim() || input.path?.trim();
173
+ if (!target) {
174
+ throw new Error("knowledge update requires --id or --path");
175
+ }
176
+ const cards = discoverKnowledgeCards(projectRoot);
177
+ const card = cards.find((item) => item.id === target || item.path === target || item.path.endsWith(`/${target}`));
178
+ if (!card) {
179
+ throw new Error(`knowledge card not found: ${target}`);
180
+ }
181
+ const fullPath = path.join(projectRoot, card.path);
182
+ const content = safeRead(fullPath);
183
+ if (content === null) {
184
+ throw new Error(`knowledge card cannot be read: ${card.path}`);
185
+ }
186
+ const parsed = parseKnowledgeFrontmatter(content);
187
+ if (!parsed.hasFrontmatter) {
188
+ throw new Error(`${card.path} must include YAML frontmatter.`);
189
+ }
190
+ const metadata = parsed.metadata;
191
+ if (!metadata.id || !metadata.title || !metadata.summary || !metadata.type || !metadata.status || !metadata.stability) {
192
+ throw new Error(`${card.path} has incomplete knowledge metadata.`);
193
+ }
194
+ const nextStatus = input.status ? parseKnowledgeStatus(input.status, "knowledge update --status") : metadata.status;
195
+ const nextStability = input.stability ? parseKnowledgeStability(input.stability, "knowledge update --stability") : metadata.stability;
196
+ const updatedAt = new Date().toISOString();
197
+ const nextMetadata = {
198
+ ...metadata,
199
+ id: metadata.id,
200
+ title: input.title?.trim() || metadata.title,
201
+ summary: input.summary?.trim() || metadata.summary,
202
+ type: metadata.type,
203
+ status: nextStatus,
204
+ stability: nextStability,
205
+ updatedAt,
206
+ scope: mergeList(metadata.scope, input.scope),
207
+ tags: mergeList(metadata.tags, input.tags),
208
+ verification: mergeList(metadata.verification, input.verification),
209
+ coverage: mergeList(metadata.coverage, input.coverage),
210
+ prdAnchors: mergeList(metadata.prdAnchors, input.anchor),
211
+ sourcePaths: mergeList(metadata.sourcePaths, input.source),
212
+ related: mergeList(metadata.related, input.related)
213
+ };
214
+ for (const key of ["rule", "workflow", "permission", "term", "note"]) {
215
+ const value = input[key]?.trim();
216
+ if (value) {
217
+ nextMetadata[key] = value;
218
+ }
219
+ }
220
+ const body = input.body?.trim() || extractMarkdownBody(content);
221
+ const errors = validateKnowledgeMetadata(card.path, nextMetadata);
222
+ if (errors.length) {
223
+ throw new Error(errors.join(" "));
224
+ }
225
+ writeText(fullPath, renderKnowledgeFromMetadata(nextMetadata, body));
226
+ return {
227
+ path: card.path,
228
+ id: nextMetadata.id,
229
+ title: nextMetadata.title,
230
+ type: nextMetadata.type,
231
+ status: nextStatus,
232
+ stability: nextStability,
233
+ createdAt: updatedAt
234
+ };
235
+ }
52
236
  export function discoverKnowledgeCards(projectRoot) {
53
237
  const knowledgeRoot = harnessPath(projectRoot, "knowledge");
54
238
  if (!isReadableDirectory(knowledgeRoot)) {
@@ -61,6 +245,133 @@ export function discoverKnowledgeCards(projectRoot) {
61
245
  })
62
246
  .sort((left, right) => left.path.localeCompare(right.path));
63
247
  }
248
+ export function indexKnowledge(projectRoot) {
249
+ const cards = discoverKnowledgeCards(projectRoot);
250
+ const prdSources = summarizePrdSources(projectRoot, cards);
251
+ const moduleSummaries = summarizeModules(cards);
252
+ const result = {
253
+ artifactPath: relativeHarnessPath("knowledge", "index.json"),
254
+ cardCount: cards.length,
255
+ activeCardCount: cards.filter((card) => card.status === "active").length,
256
+ deprecatedCardCount: cards.filter((card) => card.status === "deprecated").length,
257
+ prdSourceCount: prdSources.length,
258
+ uncoveredPrdSourceCount: prdSources.filter((source) => source.activeCardCount === 0).length,
259
+ byType: countByType(cards),
260
+ byKind: countByKind(cards),
261
+ moduleSummaries,
262
+ prdSources,
263
+ cards: cards.map(stripSearchable)
264
+ };
265
+ writeJson(path.join(projectRoot, result.artifactPath), result);
266
+ return result;
267
+ }
268
+ export function checkKnowledgeCoverage(projectRoot) {
269
+ const cards = discoverKnowledgeCards(projectRoot);
270
+ if (!isReadableDirectory(harnessPath(projectRoot, "knowledge"))) {
271
+ return {
272
+ status: "not_configured",
273
+ prdSourceCount: 0,
274
+ coveredPrdSourceCount: 0,
275
+ uncoveredPrdSources: [],
276
+ prdSourcesWithoutCoverageItems: [],
277
+ missingSourcePaths: [],
278
+ cardsWithoutSource: [],
279
+ cardsWithoutCoverage: [],
280
+ warnings: ["Create .frontend-harness/knowledge before checking project knowledge coverage."],
281
+ errors: []
282
+ };
283
+ }
284
+ const prdSources = summarizePrdSources(projectRoot, cards);
285
+ const uncoveredPrdSources = prdSources
286
+ .filter((source) => source.activeCardCount === 0)
287
+ .map((source) => source.path);
288
+ const missingSourcePaths = [...new Set(cards.flatMap((card) => card.sourcePaths)
289
+ .filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !fs.existsSync(path.join(projectRoot, sourcePath))))].sort();
290
+ const cardsWithoutSource = cards
291
+ .filter((card) => card.status === "active" && card.type === "prd_semantics" && card.sourcePaths.length === 0)
292
+ .map((card) => card.path);
293
+ const cardsWithoutCoverage = cards
294
+ .filter((card) => card.status === "active" && isPrdTraceableCard(card) && card.coverage.length === 0)
295
+ .map((card) => card.path);
296
+ const prdSourcesWithoutCoverageItems = prdSources
297
+ .filter((source) => source.activeCardCount > 0 && source.coverage.length === 0)
298
+ .map((source) => source.path);
299
+ const warnings = [
300
+ ...cardsWithoutSource.map((cardPath) => `${cardPath} has no source_paths for PRD traceability.`),
301
+ ...cardsWithoutCoverage.map((cardPath) => `${cardPath} has no coverage items for PRD block traceability.`),
302
+ ...prdSourcesWithoutCoverageItems.map((sourcePath) => `PRD source has cards but no coverage items: ${sourcePath}`),
303
+ ...missingSourcePaths.map((sourcePath) => `source path does not exist: ${sourcePath}`)
304
+ ];
305
+ const errors = [
306
+ ...uncoveredPrdSources.map((sourcePath) => `PRD source has no active knowledge cards: ${sourcePath}`),
307
+ ...prdSourcesWithoutCoverageItems.map((sourcePath) => `PRD source has no coverage items: ${sourcePath}`),
308
+ ...missingSourcePaths.map((sourcePath) => `source path does not exist: ${sourcePath}`)
309
+ ];
310
+ return {
311
+ status: errors.length || cardsWithoutSource.length || cardsWithoutCoverage.length ? "failed" : "passed",
312
+ prdSourceCount: prdSources.length,
313
+ coveredPrdSourceCount: prdSources.filter((source) => source.activeCardCount > 0).length,
314
+ uncoveredPrdSources,
315
+ prdSourcesWithoutCoverageItems,
316
+ missingSourcePaths,
317
+ cardsWithoutSource,
318
+ cardsWithoutCoverage,
319
+ warnings,
320
+ errors
321
+ };
322
+ }
323
+ export function listKnowledge(projectRoot, options = {}) {
324
+ const cards = discoverKnowledgeCards(projectRoot)
325
+ .filter((card) => options.includeDeprecated || card.status === "active")
326
+ .map(stripSearchable);
327
+ return { cards };
328
+ }
329
+ export function searchKnowledge(projectRoot, query, options = {}) {
330
+ const normalizedQuery = normalize(query ?? "");
331
+ if (!normalizedQuery) {
332
+ throw new Error("knowledge search requires --query");
333
+ }
334
+ const limit = options.limit && options.limit > 0 ? options.limit : 8;
335
+ const cards = discoverKnowledgeCards(projectRoot)
336
+ .filter((card) => options.includeDeprecated || card.status === "active")
337
+ .map((card) => ({
338
+ card,
339
+ score: scoreCardSearch(card, normalizedQuery)
340
+ }))
341
+ .filter((item) => item.score > 0)
342
+ .sort((left, right) => {
343
+ if (right.score !== left.score) {
344
+ return right.score - left.score;
345
+ }
346
+ return left.card.path.localeCompare(right.card.path);
347
+ })
348
+ .slice(0, limit)
349
+ .map(({ card, score }) => ({
350
+ ...stripSearchable(card),
351
+ score
352
+ }));
353
+ return { query: query ?? "", cards };
354
+ }
355
+ export function showKnowledge(projectRoot, idOrPath) {
356
+ const target = idOrPath?.trim();
357
+ if (!target) {
358
+ throw new Error("knowledge show requires --id or --path");
359
+ }
360
+ const cards = discoverKnowledgeCards(projectRoot);
361
+ const card = cards.find((item) => item.id === target || item.path === target || item.path.endsWith(`/${target}`));
362
+ if (!card) {
363
+ throw new Error(`knowledge card not found: ${target}`);
364
+ }
365
+ const fullPath = path.join(projectRoot, card.path);
366
+ const content = safeRead(fullPath);
367
+ if (content === null) {
368
+ throw new Error(`knowledge card cannot be read: ${card.path}`);
369
+ }
370
+ return {
371
+ card: stripSearchable(card),
372
+ body: extractMarkdownBody(content)
373
+ };
374
+ }
64
375
  export function checkKnowledge(projectRoot) {
65
376
  const knowledgeRoot = harnessPath(projectRoot, "knowledge");
66
377
  const relativeRoot = path.relative(projectRoot, knowledgeRoot).split(path.sep).join(path.posix.sep);
@@ -115,6 +426,23 @@ export function checkKnowledge(projectRoot) {
115
426
  warnings.push(`${relativePath} is deprecated without a related replacement or explanation.`);
116
427
  }
117
428
  }
429
+ if ((parsed.metadata.summary?.length ?? 0) > 180) {
430
+ warnings.push(`${relativePath} summary should stay concise for context display.`);
431
+ }
432
+ if (parsed.metadata.kind) {
433
+ const contentValue = primaryAtomicValue(parsed.metadata);
434
+ if (contentValue && normalize(contentValue) === normalize(parsed.metadata.summary ?? "")) {
435
+ warnings.push(`${relativePath} summary duplicates the primary atomic content; use summary for context and the primary field for the durable rule.`);
436
+ }
437
+ if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.length === 0) {
438
+ warnings.push(`${relativePath} should include coverage items for PRD block traceability.`);
439
+ }
440
+ }
441
+ for (const sourcePath of parsed.metadata.sourcePaths) {
442
+ if (sourcePath && !isExternalReference(sourcePath) && !fs.existsSync(path.join(projectRoot, sourcePath))) {
443
+ warnings.push(`${relativePath} source path does not exist: ${sourcePath}.`);
444
+ }
445
+ }
118
446
  }
119
447
  for (const duplicateId of duplicateIds) {
120
448
  errors.push(`Duplicate knowledge id: ${duplicateId}.`);
@@ -140,9 +468,11 @@ function describeKnowledgeCard(projectRoot, fullPath) {
140
468
  return null;
141
469
  }
142
470
  const metadata = parsed.metadata;
471
+ const body = extractMarkdownBody(content);
143
472
  const title = metadata.title;
144
473
  const summary = metadata.summary;
145
474
  const type = metadata.type;
475
+ const kind = metadata.kind;
146
476
  const status = metadata.status;
147
477
  const stability = metadata.stability;
148
478
  const scope = metadata.scope;
@@ -156,6 +486,16 @@ function describeKnowledgeCard(projectRoot, fullPath) {
156
486
  title,
157
487
  summary,
158
488
  type,
489
+ ...(kind ? { kind } : {}),
490
+ ...(metadata.subject ? { subject: metadata.subject } : {}),
491
+ ...(metadata.rule ? { rule: metadata.rule } : {}),
492
+ ...(metadata.workflow ? { workflow: metadata.workflow } : {}),
493
+ ...(metadata.permission ? { permission: metadata.permission } : {}),
494
+ ...(metadata.term ? { term: metadata.term } : {}),
495
+ ...(metadata.note ? { note: metadata.note } : {}),
496
+ verification: metadata.verification,
497
+ coverage: metadata.coverage,
498
+ prdAnchors: metadata.prdAnchors,
159
499
  status,
160
500
  stability,
161
501
  scope,
@@ -169,18 +509,37 @@ function describeKnowledgeCard(projectRoot, fullPath) {
169
509
  title,
170
510
  summary,
171
511
  type,
512
+ kind ?? "",
172
513
  stability,
514
+ metadata.subject ?? "",
515
+ metadata.rule ?? "",
516
+ metadata.workflow ?? "",
517
+ metadata.permission ?? "",
518
+ metadata.term ?? "",
519
+ metadata.note ?? "",
520
+ ...metadata.verification,
521
+ ...metadata.coverage,
522
+ ...metadata.prdAnchors,
173
523
  ...scope,
174
524
  ...tags,
175
525
  ...sourcePaths,
176
- ...related
526
+ ...related,
527
+ body
177
528
  ].join(" "))
178
529
  };
179
530
  }
531
+ function stripSearchable(card) {
532
+ const { searchable, ...publicCard } = card;
533
+ void searchable;
534
+ return publicCard;
535
+ }
180
536
  function parseKnowledgeFrontmatter(content) {
181
537
  const metadata = {
182
538
  scope: [],
183
539
  tags: [],
540
+ verification: [],
541
+ coverage: [],
542
+ prdAnchors: [],
184
543
  sourcePaths: [],
185
544
  related: []
186
545
  };
@@ -191,7 +550,7 @@ function parseKnowledgeFrontmatter(content) {
191
550
  if (endIndex < 0) {
192
551
  return { hasFrontmatter: false, metadata };
193
552
  }
194
- const listKeys = new Set(["scope", "tags", "source_paths", "related"]);
553
+ const listKeys = new Set(["scope", "tags", "verification", "coverage", "prd_anchors", "source_paths", "related"]);
195
554
  let currentList = null;
196
555
  const startIndex = content.startsWith("---\r\n") ? 5 : 4;
197
556
  for (const line of content.slice(startIndex, endIndex).split(/\r?\n/)) {
@@ -214,7 +573,7 @@ function parseKnowledgeFrontmatter(content) {
214
573
  }
215
574
  const key = pair[1] ?? "";
216
575
  const value = (pair[2] ?? "").trim();
217
- if (key === "id" || key === "title" || key === "summary" || key === "type" || key === "status" || key === "stability") {
576
+ if (key === "id" || key === "title" || key === "summary" || key === "type" || key === "kind" || key === "subject" || key === "rule" || key === "workflow" || key === "permission" || key === "term" || key === "note" || key === "status" || key === "stability") {
218
577
  metadata[key] = unquoteScalar(value);
219
578
  currentList = null;
220
579
  continue;
@@ -225,7 +584,11 @@ function parseKnowledgeFrontmatter(content) {
225
584
  continue;
226
585
  }
227
586
  if (listKeys.has(key)) {
228
- const mappedKey = key === "source_paths" ? "sourcePaths" : key;
587
+ const mappedKey = key === "source_paths"
588
+ ? "sourcePaths"
589
+ : key === "prd_anchors"
590
+ ? "prdAnchors"
591
+ : key;
229
592
  metadata[mappedKey] = parseListValue(value);
230
593
  currentList = value ? null : mappedKey;
231
594
  continue;
@@ -248,6 +611,17 @@ function validateKnowledgeMetadata(relativePath, metadata) {
248
611
  if (!metadata.type || !knowledgeTypes.has(metadata.type)) {
249
612
  errors.push(`${relativePath} type must be one of: ${[...knowledgeTypes].join(", ")}.`);
250
613
  }
614
+ if (metadata.kind) {
615
+ if (!knowledgeKinds.has(metadata.kind)) {
616
+ errors.push(`${relativePath} kind must be one of: ${[...knowledgeKinds].join(", ")}.`);
617
+ }
618
+ if (!metadata.subject) {
619
+ errors.push(`${relativePath} atomic knowledge must include subject.`);
620
+ }
621
+ if (!metadata.rule && !metadata.workflow && !metadata.permission && !metadata.term && !metadata.note) {
622
+ errors.push(`${relativePath} atomic knowledge must include rule, workflow, permission, term, or note.`);
623
+ }
624
+ }
251
625
  if (!metadata.status || !knowledgeStatuses.has(metadata.status)) {
252
626
  errors.push(`${relativePath} status must be one of: ${[...knowledgeStatuses].join(", ")}.`);
253
627
  }
@@ -259,6 +633,93 @@ function validateKnowledgeMetadata(relativePath, metadata) {
259
633
  }
260
634
  return errors;
261
635
  }
636
+ function renderKnowledgeFromMetadata(metadata, body) {
637
+ return `---
638
+ id: ${yamlScalar(metadata.id)}
639
+ type: ${yamlScalar(metadata.type)}
640
+ ${renderOptionalScalar("kind", metadata.kind)}title: ${yamlScalar(metadata.title)}
641
+ summary: ${yamlScalar(metadata.summary)}
642
+ ${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:
643
+ ${renderList(metadata.scope)}
644
+ tags:
645
+ ${renderList(metadata.tags)}
646
+ verification:
647
+ ${renderList(metadata.verification)}
648
+ coverage:
649
+ ${renderList(metadata.coverage ?? [])}
650
+ prd_anchors:
651
+ ${renderList(metadata.prdAnchors ?? [])}
652
+ status: ${yamlScalar(metadata.status)}
653
+ stability: ${yamlScalar(metadata.stability)}
654
+ updated_at: ${yamlScalar(metadata.updatedAt)}
655
+ source_paths:
656
+ ${renderList(metadata.sourcePaths)}
657
+ related:
658
+ ${renderList(metadata.related)}
659
+ ---
660
+
661
+ ${body.trim()}
662
+ `;
663
+ }
664
+ function renderAtomicKnowledge(input) {
665
+ return `---
666
+ id: ${yamlScalar(input.id)}
667
+ type: ${yamlScalar(input.type)}
668
+ kind: ${yamlScalar(input.kind)}
669
+ title: ${yamlScalar(input.title)}
670
+ summary: ${yamlScalar(input.summary)}
671
+ subject: ${yamlScalar(input.subject)}
672
+ ${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:
673
+ ${renderList(input.scope)}
674
+ tags:
675
+ ${renderList(input.tags)}
676
+ verification:
677
+ ${renderList(input.verification)}
678
+ coverage:
679
+ ${renderList(input.coverage)}
680
+ prd_anchors:
681
+ ${renderList(input.prdAnchors)}
682
+ status: ${yamlScalar(input.status)}
683
+ stability: ${yamlScalar(input.stability)}
684
+ updated_at: ${yamlScalar(input.createdAt)}
685
+ source_paths:
686
+ ${renderList(input.sourcePaths)}
687
+ related: []
688
+ ---
689
+
690
+ # ${input.title}
691
+
692
+ ${renderAtomicBody(input.kind, input.subject, input.summary, input.contentFields, input.coverage, input.prdAnchors, input.verification)}
693
+ `;
694
+ }
695
+ function renderOptionalScalar(key, value) {
696
+ return value ? `${key}: ${yamlScalar(value)}\n` : "";
697
+ }
698
+ function renderAtomicBody(kind, subject, summary, contentFields, coverage, prdAnchors, verification) {
699
+ const contentEntries = Object.entries(contentFields).filter(([, value]) => Boolean(value?.trim()));
700
+ const primaryLabel = titleCase(contentKeyForKind(kind));
701
+ const primaryValue = contentFields[contentKeyForKind(kind)] ?? contentEntries[0]?.[1];
702
+ const sections = [
703
+ "# " + titleCase(kind) + ": " + subject,
704
+ "",
705
+ "## Context",
706
+ "",
707
+ summary,
708
+ "",
709
+ "## Durable Knowledge",
710
+ "",
711
+ ...(primaryValue ? [`**${primaryLabel}**`, "", primaryValue] : [subject])
712
+ ];
713
+ const note = contentFields.note;
714
+ if (note && contentKeyForKind(kind) !== "note") {
715
+ sections.push("", "## Notes", "", note);
716
+ }
717
+ sections.push("", "## Traceability", "", ...(coverage.length ? coverage.map((item) => `- Coverage: ${item}`) : ["- Coverage: Not specified."]), ...(prdAnchors.length ? prdAnchors.map((item) => `- Anchor: ${item}`) : []));
718
+ sections.push("", "## Verification", "", ...(verification.length ? verification.map((item) => `- ${item}`) : ["- Not specified."]));
719
+ return [
720
+ ...sections
721
+ ].join("\n");
722
+ }
262
723
  function renderKnowledge(input) {
263
724
  const summary = firstBodyLine(input.body) ?? input.title;
264
725
  return `---
@@ -268,6 +729,8 @@ title: ${yamlScalar(input.title)}
268
729
  summary: ${yamlScalar(summary)}
269
730
  scope:
270
731
  ${renderList(input.scope)}
732
+ tags:
733
+ ${renderList(input.tags)}
271
734
  status: ${yamlScalar(input.status)}
272
735
  stability: ${yamlScalar(input.stability)}
273
736
  updated_at: ${yamlScalar(input.createdAt)}
@@ -308,11 +771,69 @@ function parseKnowledgeStability(value, label) {
308
771
  }
309
772
  throw new Error(`${label} must be one of: ${[...knowledgeStabilities].join(", ")}`);
310
773
  }
774
+ function parseKnowledgeKind(value, label) {
775
+ if (knowledgeKinds.has(value)) {
776
+ return value;
777
+ }
778
+ throw new Error(`${label} must be one of: ${[...knowledgeKinds].join(", ")}`);
779
+ }
780
+ function atomicContent(kind, input) {
781
+ return atomicContentFields(kind, input)[contentKeyForKind(kind)] ?? input.note?.trim() ?? null;
782
+ }
783
+ function atomicContentFields(kind, input) {
784
+ const fields = {};
785
+ const preferredKey = contentKeyForKind(kind);
786
+ const preferredValue = input[preferredKey]?.trim();
787
+ if (preferredValue) {
788
+ fields[preferredKey] = preferredValue;
789
+ }
790
+ const note = input.note?.trim();
791
+ if (note && preferredKey !== "note") {
792
+ fields.note = note;
793
+ }
794
+ return fields;
795
+ }
796
+ function summarizeAtomicContent(kind, subject, contentFields) {
797
+ const primary = contentFields[contentKeyForKind(kind)] ?? contentFields.note ?? subject;
798
+ const concise = primary.length > 140 ? `${primary.slice(0, 137)}...` : primary;
799
+ return `${subject}: ${concise}`;
800
+ }
801
+ function contentKeyForKind(kind) {
802
+ if (kind === "workflow") {
803
+ return "workflow";
804
+ }
805
+ if (kind === "permission") {
806
+ return "permission";
807
+ }
808
+ if (kind === "term") {
809
+ return "term";
810
+ }
811
+ if (kind === "pitfall" || kind === "decision" || kind === "convention") {
812
+ return "note";
813
+ }
814
+ return "rule";
815
+ }
816
+ function primaryAtomicValue(metadata) {
817
+ const kind = metadata.kind;
818
+ if (!kind || !knowledgeKinds.has(kind)) {
819
+ return undefined;
820
+ }
821
+ return metadata[contentKeyForKind(kind)] ?? metadata.note;
822
+ }
823
+ function contentFlagForKind(kind) {
824
+ return contentKeyForKind(kind);
825
+ }
311
826
  function splitList(value) {
312
827
  return value
313
828
  ? value.split(",").map((item) => item.trim()).filter(Boolean)
314
829
  : [];
315
830
  }
831
+ function mergeList(existing, next) {
832
+ if (next === undefined) {
833
+ return existing;
834
+ }
835
+ return [...new Set([...existing, ...splitList(next)])];
836
+ }
316
837
  function parseListValue(value) {
317
838
  if (!value || value === "[]") {
318
839
  return [];
@@ -336,6 +857,9 @@ function slugify(value) {
336
857
  .replace(/^-+|-+$/g, "")
337
858
  .slice(0, 80);
338
859
  }
860
+ function titleCase(value) {
861
+ return value.slice(0, 1).toUpperCase() + value.slice(1);
862
+ }
339
863
  function formatKnowledgeStamp(createdAt) {
340
864
  return createdAt
341
865
  .replace(/[-:]/g, "")
@@ -384,4 +908,117 @@ function listMarkdownFiles(directory, errors) {
384
908
  function normalize(value) {
385
909
  return value.toLowerCase().replace(/[^\p{L}\p{N}-]+/gu, " ").trim();
386
910
  }
911
+ function scoreCardSearch(card, normalizedQuery) {
912
+ const queryTokens = tokens(normalizedQuery);
913
+ if (!queryTokens.length) {
914
+ return card.searchable.includes(normalizedQuery) ? 1 : 0;
915
+ }
916
+ const searchableTokens = new Set(tokens(card.searchable));
917
+ let score = 0;
918
+ for (const token of queryTokens) {
919
+ if (searchableTokens.has(token)) {
920
+ score += 2;
921
+ }
922
+ else if (card.searchable.includes(token)) {
923
+ score += 1;
924
+ }
925
+ }
926
+ if (card.id === normalizedQuery || normalize(card.title) === normalizedQuery) {
927
+ score += 4;
928
+ }
929
+ return score;
930
+ }
931
+ function tokens(value) {
932
+ return value.split(/\s+/).filter((token) => token.length >= 2);
933
+ }
934
+ function extractMarkdownBody(content) {
935
+ const endIndex = content.search(/\r?\n---(?:\r?\n|$)/);
936
+ if (!content.startsWith("---") || endIndex < 0) {
937
+ return content.trim();
938
+ }
939
+ return content.slice(endIndex).replace(/^\r?\n---\r?\n?/, "").trim();
940
+ }
941
+ function isExternalReference(value) {
942
+ return /^[a-z][a-z0-9+.-]*:/i.test(value);
943
+ }
944
+ function summarizePrdSources(projectRoot, cards) {
945
+ const grouped = new Map();
946
+ for (const sourcePath of discoverPrdSourceFiles(projectRoot)) {
947
+ grouped.set(sourcePath, []);
948
+ }
949
+ for (const card of cards) {
950
+ for (const sourcePath of card.sourcePaths.filter(isPrdSourcePath)) {
951
+ grouped.set(sourcePath, [...(grouped.get(sourcePath) ?? []), card]);
952
+ }
953
+ }
954
+ return [...grouped.entries()]
955
+ .sort(([left], [right]) => left.localeCompare(right))
956
+ .map(([sourcePath, sourceCards]) => ({
957
+ path: sourcePath,
958
+ cardCount: sourceCards.length,
959
+ activeCardCount: sourceCards.filter((card) => card.status === "active").length,
960
+ cardIds: sourceCards.map((card) => card.id).sort(),
961
+ scopes: [...new Set(sourceCards.flatMap((card) => card.scope))].sort(),
962
+ tags: [...new Set(sourceCards.flatMap((card) => card.tags))].sort(),
963
+ coverage: [...new Set(sourceCards.flatMap((card) => card.coverage))].sort(),
964
+ prdAnchors: [...new Set(sourceCards.flatMap((card) => card.prdAnchors))].sort()
965
+ }));
966
+ }
967
+ function summarizeModules(cards) {
968
+ const atomicCards = cards.filter((card) => card.kind && card.status === "active");
969
+ return cards
970
+ .filter((card) => card.type === "module_summary" && card.status === "active")
971
+ .map((moduleCard) => {
972
+ const moduleSources = new Set(moduleCard.sourcePaths);
973
+ const relatedIds = new Set(moduleCard.related);
974
+ const relatedAtomicCards = atomicCards.filter((card) => relatedIds.has(card.id) ||
975
+ card.related.includes(moduleCard.id) ||
976
+ card.sourcePaths.some((sourcePath) => moduleSources.has(sourcePath)) ||
977
+ card.scope.some((scope) => moduleCard.scope.includes(scope)));
978
+ return {
979
+ id: moduleCard.id,
980
+ path: moduleCard.path,
981
+ title: moduleCard.title,
982
+ summary: moduleCard.summary,
983
+ sourcePaths: moduleCard.sourcePaths,
984
+ scopes: moduleCard.scope,
985
+ tags: moduleCard.tags,
986
+ related: moduleCard.related,
987
+ atomicCardCount: relatedAtomicCards.length,
988
+ atomicCardIds: relatedAtomicCards.map((card) => card.id).sort()
989
+ };
990
+ })
991
+ .sort((left, right) => left.path.localeCompare(right.path));
992
+ }
993
+ function discoverPrdSourceFiles(projectRoot) {
994
+ const roots = ["docs/prd", "docs/requirements", "prd", "requirements"];
995
+ return [...new Set(roots.flatMap((root) => {
996
+ const fullPath = path.join(projectRoot, root);
997
+ return isReadableDirectory(fullPath)
998
+ ? listMarkdownFiles(fullPath).map((filePath) => path.relative(projectRoot, filePath).replace(/\\/g, "/"))
999
+ : [];
1000
+ }))].sort();
1001
+ }
1002
+ function isPrdSourcePath(sourcePath) {
1003
+ return /(^|\/)(prd|requirements?)(\/|[-_.])|(^|\/)docs\/(prd|requirements?)(\/|$)/i.test(sourcePath);
1004
+ }
1005
+ function isPrdTraceableCard(card) {
1006
+ return card.type === "prd_semantics" || card.type === "module_summary" || card.type === "pitfall" || card.type === "glossary";
1007
+ }
1008
+ function countByType(cards) {
1009
+ const counts = Object.fromEntries([...knowledgeTypes].map((type) => [type, 0]));
1010
+ for (const card of cards) {
1011
+ counts[card.type] += 1;
1012
+ }
1013
+ return counts;
1014
+ }
1015
+ function countByKind(cards) {
1016
+ const counts = {};
1017
+ for (const card of cards) {
1018
+ if (card.kind) {
1019
+ counts[card.kind] = (counts[card.kind] ?? 0) + 1;
1020
+ }
1021
+ }
1022
+ return counts;
1023
+ }
387
1024
  //# sourceMappingURL=knowledge.js.map