frontend-harness 0.8.0 → 0.8.2
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/README.md +2 -2
- package/dist/cli/index.js +48 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/runtime/builtin-skills.js +1 -0
- package/dist/runtime/builtin-skills.js.map +1 -1
- package/dist/runtime/command-taxonomy.js +1 -1
- package/dist/runtime/command-taxonomy.js.map +1 -1
- package/dist/runtime/knowledge/core.d.ts +4 -1
- package/dist/runtime/knowledge/core.js +287 -2
- package/dist/runtime/knowledge/core.js.map +1 -1
- package/dist/runtime/knowledge/types.d.ts +58 -0
- package/dist/runtime/knowledge.d.ts +2 -2
- package/dist/runtime/knowledge.js +1 -1
- package/dist/runtime/knowledge.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,8 @@ import { kindTypeMap, knowledgeKinds, knowledgeStabilities, knowledgeStatuses, k
|
|
|
8
8
|
export const MAX_KNOWLEDGE_LIST_CARDS = 20;
|
|
9
9
|
export const MAX_KNOWLEDGE_SEARCH_RESULTS = 8;
|
|
10
10
|
export const MAX_KNOWLEDGE_DRAFT_UPDATE_CANDIDATES = 8;
|
|
11
|
+
export const MAX_KNOWLEDGE_OUTLINE_HEADINGS = 80;
|
|
12
|
+
export const MAX_KNOWLEDGE_DRAFT_EXTRACTION_ITEMS = 40;
|
|
11
13
|
export const MAX_KNOWLEDGE_INDEX_CARDS = 20;
|
|
12
14
|
export const MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS = 20;
|
|
13
15
|
const MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS = 20;
|
|
@@ -37,6 +39,9 @@ export function promoteKnowledge(projectRoot, input) {
|
|
|
37
39
|
createdAt,
|
|
38
40
|
scope: splitList(input.scope),
|
|
39
41
|
tags: splitList(input.tags),
|
|
42
|
+
verification: splitList(input.verification),
|
|
43
|
+
coverage: splitList(input.coverage),
|
|
44
|
+
prdAnchors: splitList(input.anchor),
|
|
40
45
|
sourcePaths: parseKnowledgeSourcePaths(input.source, "knowledge promote --source")
|
|
41
46
|
});
|
|
42
47
|
writeText(path.join(projectRoot, relativePath), content);
|
|
@@ -159,6 +164,24 @@ export function addKnowledge(projectRoot, input) {
|
|
|
159
164
|
createdAt
|
|
160
165
|
};
|
|
161
166
|
}
|
|
167
|
+
export function outlineKnowledgeFromIngest(projectRoot, input) {
|
|
168
|
+
if (!input.ref?.trim()) {
|
|
169
|
+
throw new Error("knowledge outline requires --ref ingest://<kind>/<source-id>");
|
|
170
|
+
}
|
|
171
|
+
const record = showIngestRecord(projectRoot, input.ref);
|
|
172
|
+
const outlineSelection = buildKnowledgeOutline(projectRoot, record);
|
|
173
|
+
return {
|
|
174
|
+
contractVersion: 1,
|
|
175
|
+
ingestRef: record.ingestRef,
|
|
176
|
+
ingestKind: record.kind,
|
|
177
|
+
source: record.source.value,
|
|
178
|
+
sourceType: record.source.type,
|
|
179
|
+
outline: outlineSelection.headings,
|
|
180
|
+
limits: {
|
|
181
|
+
headings: outlineSelection.limits
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
162
185
|
export function draftKnowledgeFromIngest(projectRoot, input) {
|
|
163
186
|
if (!input.ref?.trim()) {
|
|
164
187
|
throw new Error("knowledge draft requires --ref ingest://<kind>/<source-id>");
|
|
@@ -182,6 +205,10 @@ export function draftKnowledgeFromIngest(projectRoot, input) {
|
|
|
182
205
|
const contentFlag = contentFlagForKind(recommendedKind);
|
|
183
206
|
const updateCandidateSelection = draftUpdateCandidates(projectRoot, record, defaults);
|
|
184
207
|
const updateCandidates = updateCandidateSelection.candidates;
|
|
208
|
+
const outlineSelection = buildKnowledgeOutline(projectRoot, record);
|
|
209
|
+
const extractionItemSelection = draftExtractionItems(record, outlineSelection.headings, updateCandidates);
|
|
210
|
+
const outlineLimits = outlineSelection.limits.total > 0 ? outlineSelection.limits : undefined;
|
|
211
|
+
const extractionLimits = extractionItemSelection.limits.total > 0 ? extractionItemSelection.limits : undefined;
|
|
185
212
|
const args = [
|
|
186
213
|
"knowledge",
|
|
187
214
|
"add",
|
|
@@ -219,13 +246,20 @@ export function draftKnowledgeFromIngest(projectRoot, input) {
|
|
|
219
246
|
command: "frontend-harness knowledge add --json",
|
|
220
247
|
args,
|
|
221
248
|
updateCandidates,
|
|
249
|
+
...(outlineSelection.headings.length ? { outline: outlineSelection.headings } : {}),
|
|
250
|
+
...(extractionItemSelection.items.length ? { extractionItems: extractionItemSelection.items } : {}),
|
|
222
251
|
limits: {
|
|
223
|
-
updateCandidates: updateCandidateSelection.limits
|
|
252
|
+
updateCandidates: updateCandidateSelection.limits,
|
|
253
|
+
...(outlineLimits ? { outlineHeadings: outlineLimits } : {}),
|
|
254
|
+
...(extractionLimits ? { extractionItems: extractionLimits } : {})
|
|
224
255
|
},
|
|
225
256
|
requiredEdits: [
|
|
226
257
|
...(updateCandidateSelection.limits.total > 0
|
|
227
258
|
? ["Prefer a matching update candidate before adding a new card for the same source."]
|
|
228
259
|
: []),
|
|
260
|
+
...(extractionItemSelection.items.length
|
|
261
|
+
? ["Use extractionItems as the source checklist; add, update, or explicitly skip each required source block."]
|
|
262
|
+
: []),
|
|
229
263
|
`Replace --${contentFlag} with one atomic durable fact; create separate cards for separate facts.`,
|
|
230
264
|
"Replace --coverage with the precise source section, endpoint, screen, or acceptance block.",
|
|
231
265
|
"Replace --verification with the concrete check or evidence that proves the fact still holds.",
|
|
@@ -278,6 +312,228 @@ function draftUpdateCandidates(projectRoot, record, defaults) {
|
|
|
278
312
|
}
|
|
279
313
|
};
|
|
280
314
|
}
|
|
315
|
+
function buildKnowledgeOutline(projectRoot, record) {
|
|
316
|
+
if (record.kind !== "prd" || record.source.type !== "project-file" || !/\.md$/i.test(record.source.value)) {
|
|
317
|
+
return {
|
|
318
|
+
headings: [],
|
|
319
|
+
limits: { selected: 0, total: 0, omitted: 0 }
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const fullPath = path.join(projectRoot, record.source.value);
|
|
323
|
+
const content = safeRead(fullPath);
|
|
324
|
+
if (content === null) {
|
|
325
|
+
return {
|
|
326
|
+
headings: [],
|
|
327
|
+
limits: { selected: 0, total: 0, omitted: 0 }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const headings = parseKnowledgeOutlineHeadings(content);
|
|
331
|
+
const selected = headings.slice(0, MAX_KNOWLEDGE_OUTLINE_HEADINGS);
|
|
332
|
+
return {
|
|
333
|
+
headings: selected,
|
|
334
|
+
limits: {
|
|
335
|
+
selected: selected.length,
|
|
336
|
+
total: headings.length,
|
|
337
|
+
omitted: Math.max(0, headings.length - selected.length)
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function parseKnowledgeOutlineHeadings(content) {
|
|
342
|
+
const lines = content.split(/\r?\n/);
|
|
343
|
+
const rawHeadings = [];
|
|
344
|
+
const stack = [];
|
|
345
|
+
const idCounts = new Map();
|
|
346
|
+
lines.forEach((line, index) => {
|
|
347
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
348
|
+
if (!match?.[1] || !match[2]) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const level = match[1].length;
|
|
352
|
+
const title = match[2].replace(/\s+#+$/, "").trim();
|
|
353
|
+
if (!title) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
while (stack.length && stack[stack.length - 1].level >= level) {
|
|
357
|
+
stack.pop();
|
|
358
|
+
}
|
|
359
|
+
const baseId = slugify(title) || `heading-${index + 1}`;
|
|
360
|
+
const count = idCounts.get(baseId) ?? 0;
|
|
361
|
+
idCounts.set(baseId, count + 1);
|
|
362
|
+
const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
|
|
363
|
+
const parent = stack[stack.length - 1] ?? null;
|
|
364
|
+
const headingPath = [...(parent?.path ?? []), title];
|
|
365
|
+
rawHeadings.push({
|
|
366
|
+
id,
|
|
367
|
+
title,
|
|
368
|
+
level,
|
|
369
|
+
lineStart: index + 1,
|
|
370
|
+
parentId: parent?.id ?? null,
|
|
371
|
+
path: headingPath
|
|
372
|
+
});
|
|
373
|
+
stack.push({ id, level, title, path: headingPath });
|
|
374
|
+
});
|
|
375
|
+
return rawHeadings.map((heading, index) => {
|
|
376
|
+
const nextHeading = rawHeadings.slice(index + 1).find((candidate) => candidate.level <= heading.level);
|
|
377
|
+
const classification = classifyOutlineHeading(heading.title, heading.path);
|
|
378
|
+
return {
|
|
379
|
+
...heading,
|
|
380
|
+
lineEnd: nextHeading ? nextHeading.lineStart - 1 : lines.length,
|
|
381
|
+
extractionRequired: classification.extractionRequired,
|
|
382
|
+
suggestedKind: classification.suggestedKind,
|
|
383
|
+
reason: classification.reason,
|
|
384
|
+
coverage: heading.path.join(" > ")
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function classifyOutlineHeading(title, headingPath) {
|
|
389
|
+
const text = normalize([...headingPath, title].join(" "));
|
|
390
|
+
if (/(?:不做|不扩展|边界|风险|异常|歧义|pitfall|risk|out of scope|non goal|not doing)/i.test(text)) {
|
|
391
|
+
return {
|
|
392
|
+
extractionRequired: true,
|
|
393
|
+
suggestedKind: "pitfall",
|
|
394
|
+
reason: "source block describes boundary, exception, risk, ambiguity, or non-goal information"
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (/(?:权限|按钮|操作|可见|允许|禁止|permission|button|action|visibility)/i.test(text)) {
|
|
398
|
+
return {
|
|
399
|
+
extractionRequired: true,
|
|
400
|
+
suggestedKind: "permission",
|
|
401
|
+
reason: "source block describes action availability, visibility, or permission behavior"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (/(?:流程|链路|流转|workflow|process|journey|flow|lifecycle|状态机)/i.test(text)) {
|
|
405
|
+
return {
|
|
406
|
+
extractionRequired: true,
|
|
407
|
+
suggestedKind: "workflow",
|
|
408
|
+
reason: "source block describes workflow, lifecycle, or state transition semantics"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (/(?:字段|枚举|状态|术语|定义|field|enum|status|term|definition)/i.test(text)) {
|
|
412
|
+
return {
|
|
413
|
+
extractionRequired: true,
|
|
414
|
+
suggestedKind: "term",
|
|
415
|
+
reason: "source block defines fields, states, terms, or enumerations"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (/(?:模块|页面结构|区域|信息层级|布局|module|section|layout|region)/i.test(text)) {
|
|
419
|
+
return {
|
|
420
|
+
extractionRequired: true,
|
|
421
|
+
suggestedKind: "module_summary",
|
|
422
|
+
reason: "source block describes module, page region, layout, or structure boundaries"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (/(?:规则|校验|验收|条件|策略|计算|rule|validation|acceptance|criteria|condition|policy)/i.test(text)) {
|
|
426
|
+
return {
|
|
427
|
+
extractionRequired: true,
|
|
428
|
+
suggestedKind: "rule",
|
|
429
|
+
reason: "source block describes durable rule, validation, acceptance, condition, or policy"
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
extractionRequired: false,
|
|
434
|
+
suggestedKind: "none",
|
|
435
|
+
reason: "source block appears descriptive; mark skip if it has no durable implementation knowledge"
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function draftExtractionItems(record, headings, sourceUpdateCandidates) {
|
|
439
|
+
const requiredHeadings = headings.filter((heading) => heading.extractionRequired && heading.suggestedKind !== "none");
|
|
440
|
+
const items = requiredHeadings
|
|
441
|
+
.slice(0, MAX_KNOWLEDGE_DRAFT_EXTRACTION_ITEMS)
|
|
442
|
+
.map((heading) => draftExtractionItem(record, heading, sourceUpdateCandidates));
|
|
443
|
+
return {
|
|
444
|
+
items,
|
|
445
|
+
limits: {
|
|
446
|
+
selected: items.length,
|
|
447
|
+
total: requiredHeadings.length,
|
|
448
|
+
omitted: Math.max(0, requiredHeadings.length - items.length)
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function draftExtractionItem(record, heading, sourceUpdateCandidates) {
|
|
453
|
+
if (heading.suggestedKind === "none") {
|
|
454
|
+
throw new Error(`cannot draft extraction item for non-required heading: ${heading.id}`);
|
|
455
|
+
}
|
|
456
|
+
const suggestedKind = heading.suggestedKind === "module_summary" ? "module_summary" : heading.suggestedKind;
|
|
457
|
+
const subject = titleCase(heading.title.replace(/^\d+(?:\.\d+)*\s*/, ""));
|
|
458
|
+
const scope = draftScope(record);
|
|
459
|
+
const tags = suggestedKind === "module_summary" ? `prd,module,${scope}` : `prd,${suggestedKind},${scope}`;
|
|
460
|
+
const itemId = slugify(`${record.id}-${heading.title}`) || `${record.id}-${heading.id}`;
|
|
461
|
+
const updateCandidates = sourceUpdateCandidates.filter((candidate) => candidate.title.includes(heading.title) || candidate.summary.includes(heading.title) || candidate.id.includes(slugify(heading.title)));
|
|
462
|
+
if (suggestedKind === "module_summary") {
|
|
463
|
+
return {
|
|
464
|
+
id: itemId,
|
|
465
|
+
headingId: heading.id,
|
|
466
|
+
title: heading.title,
|
|
467
|
+
coverage: heading.coverage,
|
|
468
|
+
suggestedKind,
|
|
469
|
+
extractionRequired: true,
|
|
470
|
+
reason: heading.reason,
|
|
471
|
+
command: "frontend-harness knowledge module --json",
|
|
472
|
+
args: [
|
|
473
|
+
"knowledge",
|
|
474
|
+
"module",
|
|
475
|
+
"--json",
|
|
476
|
+
"--id",
|
|
477
|
+
itemId,
|
|
478
|
+
"--title",
|
|
479
|
+
subject,
|
|
480
|
+
"--summary",
|
|
481
|
+
`<module summary distilled from ${heading.coverage}>`,
|
|
482
|
+
"--body",
|
|
483
|
+
`<structured module facts from ${heading.coverage}>`,
|
|
484
|
+
"--source",
|
|
485
|
+
record.ingestRef,
|
|
486
|
+
"--coverage",
|
|
487
|
+
heading.coverage,
|
|
488
|
+
"--verification",
|
|
489
|
+
"<specific check or evidence required>",
|
|
490
|
+
"--scope",
|
|
491
|
+
scope,
|
|
492
|
+
"--tags",
|
|
493
|
+
tags
|
|
494
|
+
],
|
|
495
|
+
updateCandidates
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const kind = suggestedKind;
|
|
499
|
+
const contentFlag = contentFlagForKind(kind);
|
|
500
|
+
return {
|
|
501
|
+
id: itemId,
|
|
502
|
+
headingId: heading.id,
|
|
503
|
+
title: heading.title,
|
|
504
|
+
coverage: heading.coverage,
|
|
505
|
+
suggestedKind: kind,
|
|
506
|
+
extractionRequired: true,
|
|
507
|
+
reason: heading.reason,
|
|
508
|
+
command: "frontend-harness knowledge add --json",
|
|
509
|
+
args: [
|
|
510
|
+
"knowledge",
|
|
511
|
+
"add",
|
|
512
|
+
"--json",
|
|
513
|
+
"--id",
|
|
514
|
+
itemId,
|
|
515
|
+
"--kind",
|
|
516
|
+
kind,
|
|
517
|
+
"--subject",
|
|
518
|
+
subject,
|
|
519
|
+
"--summary",
|
|
520
|
+
`<summary distilled from ${heading.coverage}>`,
|
|
521
|
+
`--${contentFlag}`,
|
|
522
|
+
`<atomic ${contentFlag} distilled from ${heading.coverage}>`,
|
|
523
|
+
"--source",
|
|
524
|
+
record.ingestRef,
|
|
525
|
+
"--coverage",
|
|
526
|
+
heading.coverage,
|
|
527
|
+
"--verification",
|
|
528
|
+
"<specific check or evidence required>",
|
|
529
|
+
"--scope",
|
|
530
|
+
scope,
|
|
531
|
+
"--tags",
|
|
532
|
+
tags
|
|
533
|
+
],
|
|
534
|
+
updateCandidates
|
|
535
|
+
};
|
|
536
|
+
}
|
|
281
537
|
function recommendedKnowledgeKind(kind) {
|
|
282
538
|
if (kind === "openapi") {
|
|
283
539
|
return "workflow";
|
|
@@ -635,6 +891,9 @@ export function checkKnowledge(projectRoot) {
|
|
|
635
891
|
if ((parsed.metadata.summary?.length ?? 0) > 180) {
|
|
636
892
|
warnings.push(`${relativePath} summary should stay concise for context display.`);
|
|
637
893
|
}
|
|
894
|
+
if (parsed.metadata.status === "active" && parsed.metadata.type === "prd_semantics") {
|
|
895
|
+
errors.push(...prdSemanticMetadataErrors(relativePath, parsed.metadata));
|
|
896
|
+
}
|
|
638
897
|
if (parsed.metadata.kind) {
|
|
639
898
|
const contentValue = primaryAtomicValue(parsed.metadata);
|
|
640
899
|
if (contentValue && normalize(contentValue) === normalize(parsed.metadata.summary ?? "")) {
|
|
@@ -1026,6 +1285,7 @@ function renderAtomicBody(kind, subject, summary, contentFields, coverage, prdAn
|
|
|
1026
1285
|
}
|
|
1027
1286
|
function renderKnowledge(input) {
|
|
1028
1287
|
const summary = firstBodyLine(input.body) ?? input.title;
|
|
1288
|
+
const body = renderBodyWithTraceability(input.body, input.coverage, input.prdAnchors, input.verification);
|
|
1029
1289
|
return `---
|
|
1030
1290
|
id: ${yamlScalar(input.id)}
|
|
1031
1291
|
type: ${yamlScalar(input.type)}
|
|
@@ -1035,6 +1295,12 @@ scope:
|
|
|
1035
1295
|
${renderList(input.scope)}
|
|
1036
1296
|
tags:
|
|
1037
1297
|
${renderList(input.tags)}
|
|
1298
|
+
verification:
|
|
1299
|
+
${renderList(input.verification)}
|
|
1300
|
+
coverage:
|
|
1301
|
+
${renderList(input.coverage)}
|
|
1302
|
+
prd_anchors:
|
|
1303
|
+
${renderList(input.prdAnchors)}
|
|
1038
1304
|
status: ${yamlScalar(input.status)}
|
|
1039
1305
|
stability: ${yamlScalar(input.stability)}
|
|
1040
1306
|
updated_at: ${yamlScalar(input.createdAt)}
|
|
@@ -1045,7 +1311,7 @@ related: []
|
|
|
1045
1311
|
|
|
1046
1312
|
# ${input.title}
|
|
1047
1313
|
|
|
1048
|
-
${
|
|
1314
|
+
${body}
|
|
1049
1315
|
`;
|
|
1050
1316
|
}
|
|
1051
1317
|
function renderList(items) {
|
|
@@ -1550,6 +1816,25 @@ function isPrdSourcePath(sourcePath) {
|
|
|
1550
1816
|
function isPrdTraceableCard(card) {
|
|
1551
1817
|
return card.type === "prd_semantics" || card.type === "module_summary" || card.type === "pitfall" || card.type === "glossary";
|
|
1552
1818
|
}
|
|
1819
|
+
function prdSemanticMetadataErrors(relativePath, metadata) {
|
|
1820
|
+
const errors = [];
|
|
1821
|
+
if (metadata.sourcePaths.length === 0) {
|
|
1822
|
+
errors.push(`${relativePath} active PRD semantics must include source_paths.`);
|
|
1823
|
+
}
|
|
1824
|
+
if (metadata.coverage.length === 0) {
|
|
1825
|
+
errors.push(`${relativePath} active PRD semantics must include coverage.`);
|
|
1826
|
+
}
|
|
1827
|
+
if (metadata.scope.length === 0) {
|
|
1828
|
+
errors.push(`${relativePath} active PRD semantics must include scope.`);
|
|
1829
|
+
}
|
|
1830
|
+
if (metadata.tags.length === 0) {
|
|
1831
|
+
errors.push(`${relativePath} active PRD semantics must include tags.`);
|
|
1832
|
+
}
|
|
1833
|
+
if (metadata.verification.length === 0) {
|
|
1834
|
+
errors.push(`${relativePath} active PRD semantics must include verification.`);
|
|
1835
|
+
}
|
|
1836
|
+
return errors;
|
|
1837
|
+
}
|
|
1553
1838
|
function countByType(cards) {
|
|
1554
1839
|
const counts = Object.fromEntries([...knowledgeTypes].map((type) => [type, 0]));
|
|
1555
1840
|
for (const card of cards) {
|