frontend-harness 0.1.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 (81) hide show
  1. package/AGENTS.md +17 -5
  2. package/CLAUDE.md +17 -5
  3. package/README.md +65 -11
  4. package/dist/cli/index.js +166 -11
  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/clean.js +6 -1
  9. package/dist/runtime/clean.js.map +1 -1
  10. package/dist/runtime/command-taxonomy.js +12 -1
  11. package/dist/runtime/command-taxonomy.js.map +1 -1
  12. package/dist/runtime/common/naming.d.ts +2 -0
  13. package/dist/runtime/common/naming.js +13 -0
  14. package/dist/runtime/common/naming.js.map +1 -0
  15. package/dist/runtime/common/parsing.d.ts +11 -0
  16. package/dist/runtime/common/parsing.js +30 -0
  17. package/dist/runtime/common/parsing.js.map +1 -0
  18. package/dist/runtime/common/text.d.ts +1 -0
  19. package/dist/runtime/common/text.js +4 -0
  20. package/dist/runtime/common/text.js.map +1 -0
  21. package/dist/runtime/context.js +3 -2
  22. package/dist/runtime/context.js.map +1 -1
  23. package/dist/runtime/graph.js +1 -1
  24. package/dist/runtime/graph.js.map +1 -1
  25. package/dist/runtime/knowledge.d.ts +136 -1
  26. package/dist/runtime/knowledge.js +658 -17
  27. package/dist/runtime/knowledge.js.map +1 -1
  28. package/dist/runtime/plan/component-resolver.d.ts +8 -0
  29. package/dist/runtime/plan/component-resolver.js +350 -0
  30. package/dist/runtime/plan/component-resolver.js.map +1 -0
  31. package/dist/runtime/plan/guidance.d.ts +3 -0
  32. package/dist/runtime/plan/guidance.js +143 -0
  33. package/dist/runtime/plan/guidance.js.map +1 -0
  34. package/dist/runtime/plan/proposal.d.ts +2 -0
  35. package/dist/runtime/plan/proposal.js +251 -0
  36. package/dist/runtime/plan/proposal.js.map +1 -0
  37. package/dist/runtime/plan/workflow.d.ts +8 -0
  38. package/dist/runtime/plan/workflow.js +234 -0
  39. package/dist/runtime/plan/workflow.js.map +1 -0
  40. package/dist/runtime/plan.d.ts +4 -3
  41. package/dist/runtime/plan.js +163 -445
  42. package/dist/runtime/plan.js.map +1 -1
  43. package/dist/runtime/policy-provenance.js +30 -17
  44. package/dist/runtime/policy-provenance.js.map +1 -1
  45. package/dist/runtime/project-discovery.js +12 -4
  46. package/dist/runtime/project-discovery.js.map +1 -1
  47. package/dist/runtime/project-paths.js +5 -2
  48. package/dist/runtime/project-paths.js.map +1 -1
  49. package/dist/runtime/protocol-init.js +7 -4
  50. package/dist/runtime/protocol-init.js.map +1 -1
  51. package/dist/runtime/repair-decision.js +15 -30
  52. package/dist/runtime/repair-decision.js.map +1 -1
  53. package/dist/runtime/repair-packet.js +8 -1
  54. package/dist/runtime/repair-packet.js.map +1 -1
  55. package/dist/runtime/scaffold/vue-template.d.ts +7 -0
  56. package/dist/runtime/scaffold/vue-template.js +187 -0
  57. package/dist/runtime/scaffold/vue-template.js.map +1 -0
  58. package/dist/runtime/scaffold.d.ts +21 -0
  59. package/dist/runtime/scaffold.js +80 -0
  60. package/dist/runtime/scaffold.js.map +1 -0
  61. package/dist/runtime/skills.js +3 -3
  62. package/dist/runtime/skills.js.map +1 -1
  63. package/dist/runtime/state.d.ts +4 -2
  64. package/dist/runtime/state.js +84 -20
  65. package/dist/runtime/state.js.map +1 -1
  66. package/dist/runtime/ui-restoration.d.ts +4 -0
  67. package/dist/runtime/ui-restoration.js +38 -0
  68. package/dist/runtime/ui-restoration.js.map +1 -0
  69. package/dist/runtime/units.js +38 -2
  70. package/dist/runtime/units.js.map +1 -1
  71. package/dist/runtime/verification-commands.js +8 -4
  72. package/dist/runtime/verification-commands.js.map +1 -1
  73. package/dist/runtime/verify.js +107 -40
  74. package/dist/runtime/verify.js.map +1 -1
  75. package/dist/schemas/types.d.ts +73 -6
  76. package/dist/schemas/validation.js +21 -0
  77. package/dist/schemas/validation.js.map +1 -1
  78. package/dist/storage/json.js +6 -1
  79. package/dist/storage/json.js.map +1 -1
  80. package/docs/DIRECTION.md +1 -1
  81. package/package.json +3 -4
@@ -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,31 +509,51 @@ 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
  };
187
- if (!content.startsWith("---\n")) {
546
+ if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
188
547
  return { hasFrontmatter: false, metadata };
189
548
  }
190
- const endIndex = content.indexOf("\n---", 4);
549
+ const endIndex = content.search(/\r?\n---(?:\r?\n|$)/);
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
- for (const line of content.slice(4, endIndex).split("\n")) {
555
+ const startIndex = content.startsWith("---\r\n") ? 5 : 4;
556
+ for (const line of content.slice(startIndex, endIndex).split(/\r?\n/)) {
197
557
  const trimmed = line.trim();
198
558
  if (!trimmed || trimmed.startsWith("#")) {
199
559
  continue;
@@ -213,7 +573,7 @@ function parseKnowledgeFrontmatter(content) {
213
573
  }
214
574
  const key = pair[1] ?? "";
215
575
  const value = (pair[2] ?? "").trim();
216
- 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") {
217
577
  metadata[key] = unquoteScalar(value);
218
578
  currentList = null;
219
579
  continue;
@@ -224,7 +584,11 @@ function parseKnowledgeFrontmatter(content) {
224
584
  continue;
225
585
  }
226
586
  if (listKeys.has(key)) {
227
- const mappedKey = key === "source_paths" ? "sourcePaths" : key;
587
+ const mappedKey = key === "source_paths"
588
+ ? "sourcePaths"
589
+ : key === "prd_anchors"
590
+ ? "prdAnchors"
591
+ : key;
228
592
  metadata[mappedKey] = parseListValue(value);
229
593
  currentList = value ? null : mappedKey;
230
594
  continue;
@@ -247,6 +611,17 @@ function validateKnowledgeMetadata(relativePath, metadata) {
247
611
  if (!metadata.type || !knowledgeTypes.has(metadata.type)) {
248
612
  errors.push(`${relativePath} type must be one of: ${[...knowledgeTypes].join(", ")}.`);
249
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
+ }
250
625
  if (!metadata.status || !knowledgeStatuses.has(metadata.status)) {
251
626
  errors.push(`${relativePath} status must be one of: ${[...knowledgeStatuses].join(", ")}.`);
252
627
  }
@@ -258,18 +633,107 @@ function validateKnowledgeMetadata(relativePath, metadata) {
258
633
  }
259
634
  return errors;
260
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
+ }
261
723
  function renderKnowledge(input) {
262
724
  const summary = firstBodyLine(input.body) ?? input.title;
263
725
  return `---
264
- id: ${input.id}
265
- type: ${input.type}
266
- title: ${input.title}
267
- summary: ${summary}
726
+ id: ${yamlScalar(input.id)}
727
+ type: ${yamlScalar(input.type)}
728
+ title: ${yamlScalar(input.title)}
729
+ summary: ${yamlScalar(summary)}
268
730
  scope:
269
731
  ${renderList(input.scope)}
270
- status: ${input.status}
271
- stability: ${input.stability}
272
- updated_at: ${input.createdAt}
732
+ tags:
733
+ ${renderList(input.tags)}
734
+ status: ${yamlScalar(input.status)}
735
+ stability: ${yamlScalar(input.stability)}
736
+ updated_at: ${yamlScalar(input.createdAt)}
273
737
  source_paths:
274
738
  ${renderList(input.sourcePaths)}
275
739
  related: []
@@ -281,7 +745,7 @@ ${input.body}
281
745
  `;
282
746
  }
283
747
  function renderList(items) {
284
- return items.length ? items.map((item) => ` - ${item}`).join("\n") : " []";
748
+ return items.length ? items.map((item) => ` - ${yamlScalar(item)}`).join("\n") : " []";
285
749
  }
286
750
  function firstBodyLine(body) {
287
751
  return body
@@ -307,11 +771,69 @@ function parseKnowledgeStability(value, label) {
307
771
  }
308
772
  throw new Error(`${label} must be one of: ${[...knowledgeStabilities].join(", ")}`);
309
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
+ }
310
826
  function splitList(value) {
311
827
  return value
312
828
  ? value.split(",").map((item) => item.trim()).filter(Boolean)
313
829
  : [];
314
830
  }
831
+ function mergeList(existing, next) {
832
+ if (next === undefined) {
833
+ return existing;
834
+ }
835
+ return [...new Set([...existing, ...splitList(next)])];
836
+ }
315
837
  function parseListValue(value) {
316
838
  if (!value || value === "[]") {
317
839
  return [];
@@ -335,10 +857,16 @@ function slugify(value) {
335
857
  .replace(/^-+|-+$/g, "")
336
858
  .slice(0, 80);
337
859
  }
860
+ function titleCase(value) {
861
+ return value.slice(0, 1).toUpperCase() + value.slice(1);
862
+ }
338
863
  function formatKnowledgeStamp(createdAt) {
339
864
  return createdAt
340
865
  .replace(/[-:]/g, "")
341
- .replace(/\.\d{3}Z$/, "Z");
866
+ .replace(/\.(\d{3})Z$/, "$1Z");
867
+ }
868
+ function yamlScalar(value) {
869
+ return JSON.stringify(value);
342
870
  }
343
871
  function safeRead(filePath) {
344
872
  try {
@@ -380,4 +908,117 @@ function listMarkdownFiles(directory, errors) {
380
908
  function normalize(value) {
381
909
  return value.toLowerCase().replace(/[^\p{L}\p{N}-]+/gu, " ").trim();
382
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
+ }
383
1024
  //# sourceMappingURL=knowledge.js.map