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.
- package/AGENTS.md +8 -0
- package/CLAUDE.md +8 -0
- package/README.md +9 -0
- package/dist/cli/index.js +134 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/runtime/builtin-skills.js +10 -1
- package/dist/runtime/builtin-skills.js.map +1 -1
- package/dist/runtime/command-taxonomy.js +8 -0
- package/dist/runtime/command-taxonomy.js.map +1 -1
- package/dist/runtime/common/text.d.ts +1 -0
- package/dist/runtime/common/text.js +4 -0
- package/dist/runtime/common/text.js.map +1 -0
- package/dist/runtime/graph.js +1 -1
- package/dist/runtime/graph.js.map +1 -1
- package/dist/runtime/knowledge.d.ts +136 -1
- package/dist/runtime/knowledge.js +642 -5
- package/dist/runtime/knowledge.js.map +1 -1
- package/dist/runtime/plan/component-resolver.js +19 -13
- package/dist/runtime/plan/component-resolver.js.map +1 -1
- package/dist/runtime/plan/guidance.js +24 -0
- package/dist/runtime/plan/guidance.js.map +1 -1
- package/dist/runtime/plan/workflow.js +31 -25
- package/dist/runtime/plan/workflow.js.map +1 -1
- package/dist/runtime/plan.js +55 -12
- package/dist/runtime/plan.js.map +1 -1
- package/dist/runtime/project-discovery.js +0 -1
- package/dist/runtime/project-discovery.js.map +1 -1
- package/dist/runtime/project-paths.js +2 -2
- package/dist/runtime/project-paths.js.map +1 -1
- package/dist/runtime/repair-decision.js +1 -3
- package/dist/runtime/repair-decision.js.map +1 -1
- package/dist/runtime/scaffold/vue-template.js +31 -11
- package/dist/runtime/scaffold/vue-template.js.map +1 -1
- package/dist/runtime/state.d.ts +4 -2
- package/dist/runtime/state.js +54 -6
- package/dist/runtime/state.js.map +1 -1
- package/dist/runtime/ui-restoration.d.ts +4 -0
- package/dist/runtime/ui-restoration.js +38 -0
- package/dist/runtime/ui-restoration.js.map +1 -0
- package/dist/runtime/units.js +2 -1
- package/dist/runtime/units.js.map +1 -1
- package/dist/runtime/verify.js +37 -29
- package/dist/runtime/verify.js.map +1 -1
- package/dist/schemas/types.d.ts +9 -0
- package/dist/schemas/validation.js +18 -1
- package/dist/schemas/validation.js.map +1 -1
- package/docs/DIRECTION.md +1 -1
- 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"
|
|
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
|