frontend-harness 0.8.3 → 0.8.5

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.
@@ -13,6 +13,7 @@ export const MAX_KNOWLEDGE_DRAFT_EXTRACTION_ITEMS = 40;
13
13
  export const MAX_KNOWLEDGE_INDEX_CARDS = 20;
14
14
  export const MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS = 20;
15
15
  const MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS = 20;
16
+ const MIN_SELF_CONTAINED_PRD_CONTENT_CHARS = 80;
16
17
  export function promoteKnowledge(projectRoot, input) {
17
18
  const title = input.title?.trim();
18
19
  const body = input.body?.trim();
@@ -29,6 +30,8 @@ export function promoteKnowledge(projectRoot, input) {
29
30
  const id = slugify(title) || `knowledge-${formatKnowledgeStamp(createdAt).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`;
30
31
  const fileName = `${formatKnowledgeStamp(createdAt)}-${id}.md`;
31
32
  const relativePath = relativeHarnessPath("knowledge", fileName);
33
+ const coverage = splitList(input.coverage);
34
+ const sourcePaths = parseKnowledgeSourcePaths(projectRoot, input.source, "knowledge promote --source");
32
35
  const content = renderKnowledge({
33
36
  id,
34
37
  title,
@@ -40,9 +43,9 @@ export function promoteKnowledge(projectRoot, input) {
40
43
  scope: splitList(input.scope),
41
44
  tags: splitList(input.tags),
42
45
  verification: splitList(input.verification),
43
- coverage: splitList(input.coverage),
46
+ coverage,
44
47
  prdAnchors: splitList(input.anchor),
45
- sourcePaths: parseKnowledgeSourcePaths(input.source, "knowledge promote --source")
48
+ sourcePaths
46
49
  });
47
50
  writeText(path.join(projectRoot, relativePath), content);
48
51
  return {
@@ -75,6 +78,8 @@ export function createModuleKnowledge(projectRoot, input) {
75
78
  if (!id || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(id)) {
76
79
  throw new Error("knowledge module --id must be kebab-case when provided");
77
80
  }
81
+ const coverage = splitList(input.coverage);
82
+ const sourcePaths = parseKnowledgeSourcePaths(projectRoot, input.source, "knowledge module --source");
78
83
  const relativePath = relativeHarnessPath("knowledge", "modules", `${id}.md`);
79
84
  const fullPath = path.join(projectRoot, relativePath);
80
85
  if (fs.existsSync(fullPath)) {
@@ -91,9 +96,9 @@ export function createModuleKnowledge(projectRoot, input) {
91
96
  scope: splitList(input.scope),
92
97
  tags: splitList(input.tags),
93
98
  verification: splitList(input.verification),
94
- coverage: splitList(input.coverage),
99
+ coverage,
95
100
  prdAnchors: splitList(input.anchor),
96
- sourcePaths: parseKnowledgeSourcePaths(input.source, "knowledge module --source"),
101
+ sourcePaths,
97
102
  related: splitList(input.related)
98
103
  }, body);
99
104
  writeText(fullPath, content);
@@ -133,8 +138,9 @@ export function addKnowledge(projectRoot, input) {
133
138
  const contentFields = atomicContentFields(kind, input);
134
139
  const inputSummary = input.summary?.trim();
135
140
  const contentSummary = inputSummary || summarizeAtomicContent(kind, subject, contentFields);
136
- const sourcePaths = parseKnowledgeSourcePaths(input.source, "knowledge add --source");
141
+ const sourcePaths = parseKnowledgeSourcePaths(projectRoot, input.source, "knowledge add --source");
137
142
  const type = typeForAtomicKnowledge(kind, sourcePaths);
143
+ const coverage = splitList(input.coverage);
138
144
  const rendered = renderAtomicKnowledge({
139
145
  id,
140
146
  title,
@@ -149,7 +155,7 @@ export function addKnowledge(projectRoot, input) {
149
155
  tags: splitList(input.tags),
150
156
  sourcePaths,
151
157
  verification: splitList(input.verification),
152
- coverage: splitList(input.coverage),
158
+ coverage,
153
159
  prdAnchors: splitList(input.anchor),
154
160
  contentFields
155
161
  });
@@ -627,7 +633,7 @@ export function updateKnowledge(projectRoot, input) {
627
633
  verification: mergeList(metadata.verification, input.verification),
628
634
  coverage: mergeList(metadata.coverage, input.coverage),
629
635
  prdAnchors: mergeList(metadata.prdAnchors, input.anchor),
630
- sourcePaths: mergeKnowledgeSourcePaths(metadata.sourcePaths, input.source, "knowledge update --source"),
636
+ sourcePaths: mergeKnowledgeSourcePaths(projectRoot, metadata.sourcePaths, input.source, "knowledge update --source"),
631
637
  related: mergeList(metadata.related, input.related)
632
638
  };
633
639
  for (const key of ["rule", "workflow", "permission", "term", "note"]) {
@@ -700,63 +706,46 @@ export function checkKnowledgeCoverage(projectRoot, options = {}) {
700
706
  prdSourceCount: 0,
701
707
  coveredPrdSourceCount: 0,
702
708
  uncoveredPrdSources: [],
703
- prdSourcesWithoutCoverageItems: [],
704
709
  missingSourcePaths: [],
705
- cardsWithoutSource: [],
706
- cardsWithoutCoverage: [],
707
- cardsWithBroadCoverage: [],
708
- cardsWithTraceabilityDrift: [],
710
+ cardsWithThinContent: [],
711
+ cardsWithPlaceholderContent: [],
709
712
  warnings: ["Create .frontend-harness/knowledge before checking project knowledge coverage."],
710
713
  errors: []
711
714
  };
712
715
  }
713
716
  const prdSources = summarizePrdSources(projectRoot, cards, options.requiredPrdSources ?? []);
717
+ const activePrdKnowledgeCards = cards.filter((card) => card.status === "active" && isPrdKnowledgeQualityCard(card));
714
718
  const uncoveredPrdSources = prdSources
715
719
  .filter((source) => source.activeCardCount === 0)
716
720
  .map((source) => source.path);
721
+ const requiredPrdKnowledgeMissing = (options.requiredPrdSources?.length ?? 0) > 0 && activePrdKnowledgeCards.length === 0
722
+ ? (uncoveredPrdSources.length ? uncoveredPrdSources : options.requiredPrdSources ?? [])
723
+ : [];
717
724
  const missingSourcePaths = [...new Set(cards.flatMap((card) => card.sourcePaths)
718
725
  .filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)))].sort();
719
- const cardsWithoutSource = cards
720
- .filter((card) => card.status === "active" && card.type === "prd_semantics" && card.sourcePaths.length === 0)
721
- .map((card) => card.path);
722
- const cardsWithoutCoverage = cards
723
- .filter((card) => card.status === "active" && isPrdTraceableCard(card) && card.coverage.length === 0)
726
+ const cardsWithThinContent = cards
727
+ .filter((card) => card.status === "active" && isPrdKnowledgeQualityCard(card) && isThinSelfContainedKnowledge(card))
724
728
  .map((card) => card.path);
725
- const cardsWithBroadCoverage = cards
726
- .filter((card) => card.status === "active" && isPrdTraceableCard(card) && card.coverage.some(isBroadCoverageItem))
729
+ const cardsWithPlaceholderContent = cards
730
+ .filter((card) => card.status === "active" && isPrdKnowledgeQualityCard(card) && hasPlaceholderKnowledgeContent(card))
727
731
  .map((card) => card.path);
728
- const cardsWithTraceabilityDrift = cards
729
- .filter((card) => card.status === "active" && isPrdTraceableCard(card) && hasCoverageBodyDrift(projectRoot, card))
730
- .map((card) => card.path);
731
- const prdSourcesWithoutCoverageItems = prdSources
732
- .filter((source) => source.activeCardCount > 0 && source.coverage.length === 0)
733
- .map((source) => source.path);
734
732
  const warnings = [
735
- ...cardsWithoutSource.map((cardPath) => `${cardPath} has no source_paths for PRD traceability.`),
736
- ...cardsWithoutCoverage.map((cardPath) => `${cardPath} has no coverage items for PRD block traceability.`),
737
- ...cardsWithBroadCoverage.map((cardPath) => `${cardPath} uses broad PRD coverage; split PRD semantics into atomic cards with precise section, table, rule, or acceptance-block coverage.`),
738
- ...cardsWithTraceabilityDrift.map((cardPath) => `${cardPath} frontmatter coverage disagrees with body traceability.`),
739
- ...prdSourcesWithoutCoverageItems.map((sourcePath) => `PRD source has cards but no coverage items: ${sourcePath}`),
733
+ ...uncoveredPrdSources.map((sourcePath) => `PRD source has no active knowledge cards: ${sourcePath}`),
740
734
  ...missingSourcePaths.map((sourcePath) => `source path does not exist: ${sourcePath}`)
741
735
  ];
742
736
  const errors = [
743
- ...uncoveredPrdSources.map((sourcePath) => `PRD source has no active knowledge cards: ${sourcePath}`),
744
- ...prdSourcesWithoutCoverageItems.map((sourcePath) => `PRD source has no coverage items: ${sourcePath}`),
745
- ...cardsWithBroadCoverage.map((cardPath) => `${cardPath} uses broad PRD coverage instead of atomic traceability.`),
746
- ...cardsWithTraceabilityDrift.map((cardPath) => `${cardPath} has frontmatter coverage but body traceability says coverage is not specified.`),
747
- ...missingSourcePaths.map((sourcePath) => `source path does not exist: ${sourcePath}`)
737
+ ...requiredPrdKnowledgeMissing.map((sourcePath) => `PRD input has no active self-contained knowledge cards: ${sourcePath}`),
738
+ ...cardsWithThinContent.map((cardPath) => `${cardPath} is too thin to replace deleted PRD source material; keep self-contained implementation constraints.`),
739
+ ...cardsWithPlaceholderContent.map((cardPath) => `${cardPath} still contains scaffold placeholder text instead of durable knowledge.`)
748
740
  ];
749
741
  return {
750
- status: errors.length || cardsWithoutSource.length || cardsWithoutCoverage.length ? "failed" : "passed",
742
+ status: errors.length ? "failed" : "passed",
751
743
  prdSourceCount: prdSources.length,
752
744
  coveredPrdSourceCount: prdSources.filter((source) => source.activeCardCount > 0).length,
753
745
  uncoveredPrdSources,
754
- prdSourcesWithoutCoverageItems,
755
746
  missingSourcePaths,
756
- cardsWithoutSource,
757
- cardsWithoutCoverage,
758
- cardsWithBroadCoverage,
759
- cardsWithTraceabilityDrift,
747
+ cardsWithThinContent,
748
+ cardsWithPlaceholderContent,
760
749
  warnings,
761
750
  errors
762
751
  };
@@ -908,15 +897,6 @@ export function checkKnowledge(projectRoot) {
908
897
  for (const verification of parsed.metadata.verification.filter(isDraftPlaceholderVerification)) {
909
898
  errors.push(`${relativePath} verification contains an unedited knowledge draft placeholder: ${verification}.`);
910
899
  }
911
- if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.length === 0) {
912
- warnings.push(`${relativePath} should include coverage items for PRD block traceability.`);
913
- }
914
- if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.some(isBroadCoverageItem)) {
915
- warnings.push(`${relativePath} uses broad PRD coverage; split PRD semantics into smaller atomic cards with precise section, table, rule, or acceptance-block coverage.`);
916
- }
917
- }
918
- if (parsed.metadata.coverage.length > 0 && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content))) {
919
- warnings.push(`${relativePath} frontmatter coverage disagrees with body traceability.`);
920
900
  }
921
901
  for (const sourcePath of parsed.metadata.sourcePaths) {
922
902
  if (sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)) {
@@ -1177,44 +1157,23 @@ function validateKnowledgeMetadata(relativePath, metadata) {
1177
1157
  return errors;
1178
1158
  }
1179
1159
  function renderKnowledgeFromMetadata(metadata, body) {
1180
- const renderedBody = renderBodyWithTraceability(body, metadata.coverage ?? [], metadata.prdAnchors ?? [], metadata.verification ?? []);
1160
+ const renderedBody = renderBodyWithVerification(body, metadata.verification ?? []);
1181
1161
  return `---
1182
1162
  id: ${yamlScalar(metadata.id)}
1183
1163
  type: ${yamlScalar(metadata.type)}
1184
1164
  ${renderOptionalScalar("kind", metadata.kind)}title: ${yamlScalar(metadata.title)}
1185
1165
  summary: ${yamlScalar(metadata.summary)}
1186
- ${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:
1187
- ${renderList(metadata.scope)}
1188
- tags:
1189
- ${renderList(metadata.tags)}
1190
- verification:
1191
- ${renderList(metadata.verification)}
1192
- coverage:
1193
- ${renderList(metadata.coverage ?? [])}
1194
- prd_anchors:
1195
- ${renderList(metadata.prdAnchors ?? [])}
1196
- status: ${yamlScalar(metadata.status)}
1166
+ ${renderOptionalScalar("subject", metadata.subject)}${renderOptionalScalar("rule", metadata.rule)}${renderOptionalScalar("workflow", metadata.workflow)}${renderOptionalScalar("permission", metadata.permission)}${renderOptionalScalar("term", metadata.term)}${renderOptionalList("scope", metadata.scope)}${renderOptionalList("tags", metadata.tags)}${renderOptionalList("verification", metadata.verification)}${renderOptionalList("coverage", metadata.coverage ?? [])}${renderOptionalList("prd_anchors", metadata.prdAnchors ?? [])}status: ${yamlScalar(metadata.status)}
1197
1167
  stability: ${yamlScalar(metadata.stability)}
1198
1168
  updated_at: ${yamlScalar(metadata.updatedAt)}
1199
- source_paths:
1200
- ${renderList(metadata.sourcePaths)}
1201
- related:
1202
- ${renderList(metadata.related)}
1169
+ ${renderOptionalList("source_paths", metadata.sourcePaths)}${renderOptionalList("related", metadata.related)}
1203
1170
  ---
1204
1171
 
1205
1172
  ${renderedBody}
1206
1173
  `;
1207
1174
  }
1208
- function renderBodyWithTraceability(body, coverage, prdAnchors, verification) {
1175
+ function renderBodyWithVerification(body, verification) {
1209
1176
  const sections = [body.trim()];
1210
- if ((coverage.length || prdAnchors.length) && !/^## Traceability\b/m.test(body)) {
1211
- sections.push([
1212
- "## Traceability",
1213
- "",
1214
- ...(coverage.length ? coverage.map((item) => `- Coverage: ${item}`) : ["- Coverage: Not specified."]),
1215
- ...(prdAnchors.length ? prdAnchors.map((item) => `- Anchor: ${item}`) : [])
1216
- ].join("\n"));
1217
- }
1218
1177
  if (verification.length && !/^## Verification\b/m.test(body)) {
1219
1178
  sections.push([
1220
1179
  "## Verification",
@@ -1232,33 +1191,24 @@ kind: ${yamlScalar(input.kind)}
1232
1191
  title: ${yamlScalar(input.title)}
1233
1192
  summary: ${yamlScalar(input.summary)}
1234
1193
  subject: ${yamlScalar(input.subject)}
1235
- ${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:
1236
- ${renderList(input.scope)}
1237
- tags:
1238
- ${renderList(input.tags)}
1239
- verification:
1240
- ${renderList(input.verification)}
1241
- coverage:
1242
- ${renderList(input.coverage)}
1243
- prd_anchors:
1244
- ${renderList(input.prdAnchors)}
1245
- status: ${yamlScalar(input.status)}
1194
+ ${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)}${renderOptionalList("scope", input.scope)}${renderOptionalList("tags", input.tags)}${renderOptionalList("verification", input.verification)}${renderOptionalList("coverage", input.coverage)}${renderOptionalList("prd_anchors", input.prdAnchors)}status: ${yamlScalar(input.status)}
1246
1195
  stability: ${yamlScalar(input.stability)}
1247
1196
  updated_at: ${yamlScalar(input.createdAt)}
1248
- source_paths:
1249
- ${renderList(input.sourcePaths)}
1250
- related: []
1197
+ ${renderOptionalList("source_paths", input.sourcePaths)}
1251
1198
  ---
1252
1199
 
1253
1200
  # ${input.title}
1254
1201
 
1255
- ${renderAtomicBody(input.kind, input.subject, input.summary, input.contentFields, input.coverage, input.prdAnchors, input.verification)}
1202
+ ${renderAtomicBody(input.kind, input.subject, input.summary, input.contentFields, input.verification)}
1256
1203
  `;
1257
1204
  }
1258
1205
  function renderOptionalScalar(key, value) {
1259
1206
  return value ? `${key}: ${yamlScalar(value)}\n` : "";
1260
1207
  }
1261
- function renderAtomicBody(kind, subject, summary, contentFields, coverage, prdAnchors, verification) {
1208
+ function renderOptionalList(key, items) {
1209
+ return items.length ? `${key}:\n${renderList(items)}\n` : "";
1210
+ }
1211
+ function renderAtomicBody(kind, subject, summary, contentFields, verification) {
1262
1212
  const contentEntries = Object.entries(contentFields).filter(([, value]) => Boolean(value?.trim()));
1263
1213
  const primaryLabel = titleCase(contentKeyForKind(kind));
1264
1214
  const primaryValue = contentFields[contentKeyForKind(kind)] ?? contentEntries[0]?.[1];
@@ -1277,36 +1227,25 @@ function renderAtomicBody(kind, subject, summary, contentFields, coverage, prdAn
1277
1227
  if (note && contentKeyForKind(kind) !== "note") {
1278
1228
  sections.push("", "## Notes", "", note);
1279
1229
  }
1280
- sections.push("", "## Traceability", "", ...(coverage.length ? coverage.map((item) => `- Coverage: ${item}`) : ["- Coverage: Not specified."]), ...(prdAnchors.length ? prdAnchors.map((item) => `- Anchor: ${item}`) : []));
1281
- sections.push("", "## Verification", "", ...(verification.length ? verification.map((item) => `- ${item}`) : ["- Not specified."]));
1230
+ if (verification.length) {
1231
+ sections.push("", "## Verification", "", ...verification.map((item) => `- ${item}`));
1232
+ }
1282
1233
  return [
1283
1234
  ...sections
1284
1235
  ].join("\n");
1285
1236
  }
1286
1237
  function renderKnowledge(input) {
1287
1238
  const summary = firstBodyLine(input.body) ?? input.title;
1288
- const body = renderBodyWithTraceability(input.body, input.coverage, input.prdAnchors, input.verification);
1239
+ const body = renderBodyWithVerification(input.body, input.verification);
1289
1240
  return `---
1290
1241
  id: ${yamlScalar(input.id)}
1291
1242
  type: ${yamlScalar(input.type)}
1292
1243
  title: ${yamlScalar(input.title)}
1293
1244
  summary: ${yamlScalar(summary)}
1294
- scope:
1295
- ${renderList(input.scope)}
1296
- tags:
1297
- ${renderList(input.tags)}
1298
- verification:
1299
- ${renderList(input.verification)}
1300
- coverage:
1301
- ${renderList(input.coverage)}
1302
- prd_anchors:
1303
- ${renderList(input.prdAnchors)}
1304
- status: ${yamlScalar(input.status)}
1245
+ ${renderOptionalList("scope", input.scope)}${renderOptionalList("tags", input.tags)}${renderOptionalList("verification", input.verification)}${renderOptionalList("coverage", input.coverage)}${renderOptionalList("prd_anchors", input.prdAnchors)}status: ${yamlScalar(input.status)}
1305
1246
  stability: ${yamlScalar(input.stability)}
1306
1247
  updated_at: ${yamlScalar(input.createdAt)}
1307
- source_paths:
1308
- ${renderList(input.sourcePaths)}
1309
- related: []
1248
+ ${renderOptionalList("source_paths", input.sourcePaths)}
1310
1249
  ---
1311
1250
 
1312
1251
  # ${input.title}
@@ -1398,25 +1337,35 @@ function splitList(value) {
1398
1337
  ? value.split(",").map((item) => item.trim()).filter(Boolean)
1399
1338
  : [];
1400
1339
  }
1401
- function parseKnowledgeSourcePaths(value, label) {
1402
- const sourcePaths = splitList(value);
1340
+ function parseKnowledgeSourcePaths(projectRoot, value, label) {
1341
+ const sourcePaths = canonicalKnowledgeInputSourcePaths(projectRoot, splitList(value));
1403
1342
  const invalid = sourcePaths.find(validateKnowledgeSourcePath);
1404
1343
  if (invalid) {
1405
1344
  throw new Error(`${label} must contain safe project-relative paths or external references: ${invalid}`);
1406
1345
  }
1407
1346
  return sourcePaths;
1408
1347
  }
1409
- function mergeKnowledgeSourcePaths(existing, next, label) {
1348
+ function mergeKnowledgeSourcePaths(projectRoot, existing, next, label) {
1410
1349
  if (next === undefined) {
1411
1350
  return existing;
1412
1351
  }
1413
- const merged = [...new Set([...existing, ...splitList(next)])];
1352
+ const merged = [...new Set(canonicalKnowledgeInputSourcePaths(projectRoot, [...existing, ...splitList(next)]))];
1414
1353
  const invalid = merged.find(validateKnowledgeSourcePath);
1415
1354
  if (invalid) {
1416
1355
  throw new Error(`${label} must contain safe project-relative paths or external references: ${invalid}`);
1417
1356
  }
1418
1357
  return merged;
1419
1358
  }
1359
+ function canonicalKnowledgeInputSourcePaths(projectRoot, sourcePaths) {
1360
+ if (sourcePaths.length === 0) {
1361
+ return [];
1362
+ }
1363
+ const records = listIngestRecords(projectRoot, undefined, { limit: "all" }).records;
1364
+ return sourcePaths.map((sourcePath) => {
1365
+ const record = records.find((item) => sameKnowledgeSourceReference(item.ingestRef, sourcePath) || sameKnowledgeSourceReference(item.source.value, sourcePath));
1366
+ return record?.ingestRef ?? sourcePath;
1367
+ });
1368
+ }
1420
1369
  function validateKnowledgeSourcePath(value) {
1421
1370
  if (!value.trim()) {
1422
1371
  return "empty";
@@ -1664,26 +1613,6 @@ function parseIngestReference(value) {
1664
1613
  id: match[2]
1665
1614
  };
1666
1615
  }
1667
- function isBroadCoverageItem(value) {
1668
- const normalized = value.trim().toLowerCase();
1669
- if (!normalized) {
1670
- return false;
1671
- }
1672
- if (/\bsections?\s+\d+(?:\.\d+)?\s*[-–—~]\s*\d+(?:\.\d+)?\b/.test(normalized)) {
1673
- return true;
1674
- }
1675
- if (/(?:^|[#\s/])\d+(?:\.\d+)?\s*[-–—~至到]\s*\d+(?:\.\d+)?(?:\D|$)/.test(normalized)) {
1676
- return true;
1677
- }
1678
- return /\b(chapter|section)\b.*\b(full|entire|whole|all)\b/.test(normalized);
1679
- }
1680
- function hasCoverageBodyDrift(projectRoot, card) {
1681
- if (card.coverage.length === 0) {
1682
- return false;
1683
- }
1684
- const content = safeRead(path.join(projectRoot, card.path));
1685
- return content !== null && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content));
1686
- }
1687
1616
  function isDraftPlaceholderCoverage(value) {
1688
1617
  const normalized = value.trim().toLowerCase();
1689
1618
  return (normalized.startsWith("<") || normalized.endsWith(">"))
@@ -1700,13 +1629,9 @@ function isDraftPlaceholderVerification(value) {
1700
1629
  }
1701
1630
  function ingestLinkedAtomicMetadataErrors(relativePath, metadata) {
1702
1631
  const errors = [];
1703
- const requiredLists = [
1704
- ["coverage", metadata.coverage]
1705
- ];
1706
- for (const [field, values] of requiredLists) {
1707
- if (values.length === 0) {
1708
- errors.push(`${relativePath} active ingest-linked atomic knowledge must include ${field}.`);
1709
- }
1632
+ const primaryContent = primaryAtomicValue(metadata);
1633
+ if (primaryContent && hasPlaceholderText(primaryContent)) {
1634
+ errors.push(`${relativePath} active ingest-linked atomic knowledge must replace scaffold placeholder content.`);
1710
1635
  }
1711
1636
  return errors;
1712
1637
  }
@@ -1820,16 +1745,30 @@ function isPrdSourcePath(sourcePath) {
1820
1745
  }
1821
1746
  return /(^|\/)docs\/[^/]*(?:prd|requirement|需求)[^/]*\.md$/i.test(normalized);
1822
1747
  }
1823
- function isPrdTraceableCard(card) {
1824
- return card.type === "prd_semantics" || card.type === "module_summary" || card.type === "pitfall" || card.type === "glossary";
1748
+ function isPrdKnowledgeQualityCard(card) {
1749
+ return card.type === "prd_semantics" || card.type === "module_summary";
1750
+ }
1751
+ function isThinSelfContainedKnowledge(card) {
1752
+ return normalize(primaryCardContent(card)).length < MIN_SELF_CONTAINED_PRD_CONTENT_CHARS;
1753
+ }
1754
+ function hasPlaceholderKnowledgeContent(card) {
1755
+ return hasPlaceholderText(primaryCardContent(card));
1756
+ }
1757
+ function primaryCardContent(card) {
1758
+ return card.rule ?? card.workflow ?? card.permission ?? card.term ?? card.note ?? card.summary;
1759
+ }
1760
+ function hasPlaceholderText(value) {
1761
+ const normalized = value.trim().toLowerCase();
1762
+ return /^<.*>$/.test(normalized) || normalized.includes(" distilled from ") || normalized.includes("scaffold placeholder");
1825
1763
  }
1826
1764
  function prdSemanticMetadataErrors(relativePath, metadata) {
1827
1765
  const errors = [];
1828
- if (metadata.sourcePaths.length === 0) {
1829
- errors.push(`${relativePath} active PRD semantics must include source_paths.`);
1766
+ const content = primaryAtomicValue(metadata) ?? metadata.summary ?? "";
1767
+ if (normalize(content).length < MIN_SELF_CONTAINED_PRD_CONTENT_CHARS) {
1768
+ errors.push(`${relativePath} active PRD semantics must be self-contained enough to replace deleted PRD source material.`);
1830
1769
  }
1831
- if (metadata.coverage.length === 0) {
1832
- errors.push(`${relativePath} active PRD semantics must include coverage.`);
1770
+ if (hasPlaceholderText(content)) {
1771
+ errors.push(`${relativePath} active PRD semantics must replace scaffold placeholder content.`);
1833
1772
  }
1834
1773
  return errors;
1835
1774
  }